diff --git a/.vscode/settings.json b/.vscode/settings.json index 05d23f3bb..9ed1c8fc2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,8 @@ "eslint.format.enable": true, "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" - } + }, + "cSpell.words": [ + "salix" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index d677b80e3..30afaa69b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [2342.01] - 2023-10-19 +## [2348.01] - 2023-11-30 ### Added ### Changed ### Fixed -## [2340.01] - 2023-10-05 +## [2346.01] - 2023-11-16 + +### Added +### Changed +### Fixed + + +## [2342.01] - 2023-11-02 ### Added - (Usuarios -> Foto) Se muestra la foto del trabajador - -### Changed ### Fixed - (Usuarios -> Historial) Abre el descriptor del usuario correctamente + +## [2340.01] - 2023-10-05 + ## [2338.01] - 2023-09-21 ### Added @@ -29,17 +37,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - (Trabajadores -> Calendario) Icono de check arreglado cuando pulsas un tipo de dia -### Fixed - ## [2336.01] - 2023-09-07 -### Added - -### Changed - -### Fixed - - ## [2334.01] - 2023-08-24 ### Added diff --git a/back/methods/chat/sendCheckingPresence.js b/back/methods/chat/sendCheckingPresence.js index 274ec3a5b..85b66e94b 100644 --- a/back/methods/chat/sendCheckingPresence.js +++ b/back/methods/chat/sendCheckingPresence.js @@ -26,15 +26,14 @@ module.exports = Self => { Self.sendCheckingPresence = async(ctx, recipientId, message) => { if (!recipientId) return false; - const models = Self.app.models; + const userId = ctx.req.accessToken.userId; const sender = await models.VnUser.findById(userId, {fields: ['id']}); const recipient = await models.VnUser.findById(recipientId, null); // Prevent sending messages to yourself if (recipientId == userId) return false; - if (!recipient) throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`); diff --git a/back/methods/collection/getTickets.js b/back/methods/collection/getTickets.js index 445fc070d..f04822697 100644 --- a/back/methods/collection/getTickets.js +++ b/back/methods/collection/getTickets.js @@ -26,7 +26,7 @@ module.exports = Self => { Self.getTickets = async(ctx, id, print, options) => { const userId = ctx.req.accessToken.userId; - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const $t = ctx.req.__; const myOptions = {}; @@ -36,7 +36,6 @@ module.exports = Self => { myOptions.userId = userId; const promises = []; - const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions); const sales = await Self.rawSql(` SELECT s.ticketFk, @@ -86,24 +85,19 @@ module.exports = Self => { if (tickets && tickets.length) { for (const ticket of tickets) { const ticketId = ticket.ticketFk; - - // SEND ROCKET if (ticket.observaciones != '') { for (observation of ticket.observaciones.split(' ')) { if (['#', '@'].includes(observation.charAt(0))) { promises.push(Self.app.models.Chat.send(ctx, observation, $t('The ticket is in preparation', { ticketId: ticketId, - ticketUrl: `${origin}/#!/ticket/${ticketId}/summary`, + ticketUrl: `${url}ticket/${ticketId}/summary`, salesPersonId: ticket.salesPersonFk }))); } } } - - // SET COLLECTION if (sales && sales.length) { - // GET BARCODES const barcodes = await Self.rawSql(` SELECT s.id saleFk, b.code, c.id FROM vn.sale s @@ -114,13 +108,10 @@ module.exports = Self => { WHERE s.ticketFk = ? AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`, [ticketId], myOptions); - - // BINDINGS ticket.sales = []; for (const sale of sales) { if (sale.ticketFk === ticketId) { sale.Barcodes = []; - if (barcodes && barcodes.length) { for (const barcode of barcodes) { if (barcode.saleFk === sale.saleFk) { @@ -131,7 +122,6 @@ module.exports = Self => { } } } - ticket.sales.push(sale); } } @@ -140,7 +130,6 @@ module.exports = Self => { } } await Promise.all(promises); - return collection; }; }; diff --git a/back/methods/collection/spec/setSaleQuantity.spec.js b/back/methods/collection/spec/setSaleQuantity.spec.js index fdc1bce1a..b563f5b19 100644 --- a/back/methods/collection/spec/setSaleQuantity.spec.js +++ b/back/methods/collection/spec/setSaleQuantity.spec.js @@ -18,6 +18,14 @@ describe('setSaleQuantity()', () => { it('should change quantity sale', async() => { const tx = await models.Ticket.beginTransaction({}); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY + SELECT 100 as available;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); try { const options = {transaction: tx}; diff --git a/back/methods/notification/getList.js b/back/methods/notification/getList.js new file mode 100644 index 000000000..3881f0f63 --- /dev/null +++ b/back/methods/notification/getList.js @@ -0,0 +1,54 @@ +module.exports = Self => { + Self.remoteMethod('getList', { + description: 'Get list of the available and active notification subscriptions', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'User to modify', + http: {source: 'path'} + } + ], + returns: { + type: 'object', + root: true + }, + http: { + path: `/:id/getList`, + verb: 'GET' + } + }); + + Self.getList = async(id, options) => { + const activeNotificationsMap = new Map(); + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const availableNotificationsMap = await Self.getAvailable(id, myOptions); + const activeNotifications = await Self.app.models.NotificationSubscription.find({ + fields: ['id', 'notificationFk'], + include: {relation: 'notification'}, + where: {userFk: id} + }, myOptions); + + for (active of activeNotifications) { + activeNotificationsMap.set(active.notificationFk, { + id: active.id, + notificationFk: active.notificationFk, + name: active.notification().name, + description: active.notification().description, + active: true + }); + availableNotificationsMap.delete(active.notificationFk); + } + + return { + active: [...activeNotificationsMap.entries()], + available: [...availableNotificationsMap.entries()] + }; + }; +}; diff --git a/back/methods/notification/specs/getList.spec.js b/back/methods/notification/specs/getList.spec.js new file mode 100644 index 000000000..52ac497a5 --- /dev/null +++ b/back/methods/notification/specs/getList.spec.js @@ -0,0 +1,13 @@ +const models = require('vn-loopback/server/server').models; + +describe('NotificationSubscription getList()', () => { + it('should return a list of available and active notifications of a user', async() => { + const userId = 9; + const {active, available} = await models.NotificationSubscription.getList(userId); + const notifications = await models.Notification.find({}); + const totalAvailable = notifications.length - active.length; + + expect(active.length).toEqual(2); + expect(available.length).toEqual(totalAvailable); + }); +}); diff --git a/back/methods/url/getByUser.js b/back/methods/url/getByUser.js new file mode 100644 index 000000000..dd4805182 --- /dev/null +++ b/back/methods/url/getByUser.js @@ -0,0 +1,40 @@ +module.exports = function(Self) { + Self.remoteMethod('getByUser', { + description: 'returns the starred modules for the current user', + accessType: 'READ', + accepts: [{ + arg: 'userId', + type: 'number', + description: 'The user id', + required: true, + http: {source: 'path'} + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/:userId/get-by-user`, + verb: 'GET' + } + }); + + Self.getByUser = async userId => { + const models = Self.app.models; + const appNames = ['hedera']; + const filter = { + fields: ['appName', 'url'], + where: { + appName: {inq: appNames}, + environment: process.env.NODE_ENV ?? 'development', + } + }; + + const isWorker = await models.Account.findById(userId, {fields: ['id']}); + if (!isWorker) + return models.Url.find(filter); + + appNames.push('salix'); + return models.Url.find(filter); + }; +}; diff --git a/back/methods/url/getUrl.js b/back/methods/url/getUrl.js new file mode 100644 index 000000000..f30719b9f --- /dev/null +++ b/back/methods/url/getUrl.js @@ -0,0 +1,30 @@ +module.exports = Self => { + Self.remoteMethod('getUrl', { + description: 'Returns the colling app name', + accessType: 'READ', + accepts: [ + { + arg: 'app', + type: 'string', + required: false + } + ], + returns: { + type: 'object', + root: true + }, + http: { + path: `/getUrl`, + verb: 'get' + } + }); + Self.getUrl = async(appName = 'salix') => { + const {url} = await Self.app.models.Url.findOne({ + where: { + appName, + enviroment: process.env.NODE_ENV || 'development' + } + }); + return url; + }; +}; diff --git a/back/methods/url/specs/getByUser.spec.js b/back/methods/url/specs/getByUser.spec.js new file mode 100644 index 000000000..f6af6ec00 --- /dev/null +++ b/back/methods/url/specs/getByUser.spec.js @@ -0,0 +1,19 @@ +const {models} = require('vn-loopback/server/server'); + +describe('getByUser()', () => { + const worker = 1; + const notWorker = 2; + it(`should return only hedera url if not is worker`, async() => { + const urls = await models.Url.getByUser(notWorker); + + expect(urls.length).toEqual(1); + expect(urls[0].appName).toEqual('hedera'); + }); + + it(`should return more than hedera url`, async() => { + const urls = await models.Url.getByUser(worker); + + expect(urls.length).toBeGreaterThan(1); + expect(urls.find(url => url.appName == 'salix').appName).toEqual('salix'); + }); +}); diff --git a/back/methods/vn-user/update-user.js b/back/methods/vn-user/update-user.js new file mode 100644 index 000000000..ddaae8548 --- /dev/null +++ b/back/methods/vn-user/update-user.js @@ -0,0 +1,39 @@ +module.exports = Self => { + Self.remoteMethodCtx('updateUser', { + description: 'Update user data', + accepts: [ + { + arg: 'id', + type: 'integer', + description: 'The user id', + required: true, + http: {source: 'path'} + }, { + arg: 'name', + type: 'string', + description: 'The user name', + }, { + arg: 'nickname', + type: 'string', + description: 'The user nickname', + }, { + arg: 'email', + type: 'string', + description: 'The user email' + }, { + arg: 'lang', + type: 'string', + description: 'The user lang' + } + ], + http: { + path: `/:id/update-user`, + verb: 'PATCH' + } + }); + + Self.updateUser = async(ctx, id, name, nickname, email, lang) => { + await Self.userSecurity(ctx, id); + await Self.upsertWithWhere({id}, {name, nickname, email, lang}); + }; +}; diff --git a/back/models/chat.js b/back/models/chat.js index a18edbd3f..882db747e 100644 --- a/back/models/chat.js +++ b/back/models/chat.js @@ -7,17 +7,14 @@ module.exports = Self => { Self.observe('before save', async function(ctx) { if (!ctx.isNewInstance) return; - let {message} = ctx.instance; if (!message) return; const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g); if (!parts) return; - const replacedParts = parts.map(part => { return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, ''); }); - for (const [index, part] of parts.entries()) message = message.replace(part, replacedParts[index]); diff --git a/back/models/notificationSubscription.js b/back/models/notificationSubscription.js index f1b2811fa..8efb83e7d 100644 --- a/back/models/notificationSubscription.js +++ b/back/models/notificationSubscription.js @@ -1,62 +1,74 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { + require('../methods/notification/getList')(Self); + Self.observe('before save', async function(ctx) { + await checkModifyPermission(ctx); + }); + + Self.observe('before delete', async function(ctx) { + await checkModifyPermission(ctx); + }); + + async function checkModifyPermission(ctx) { const models = Self.app.models; + const instance = ctx.instance; const userId = ctx.options.accessToken.userId; - const user = await ctx.instance.userFk; - const modifiedUser = await getUserToModify(null, user, models); - if (userId != modifiedUser.id && userId != modifiedUser.bossFk) - throw new UserError('You dont have permission to modify this user'); - }); + let notificationFk; + let workerId; - Self.remoteMethod('deleteNotification', { - description: 'Deletes a notification subscription', - accepts: [ - { - arg: 'ctx', - type: 'object', - http: {source: 'context'} - }, - { - arg: 'notificationId', - type: 'number', - required: true - }, - ], - returns: { - type: 'object', - root: true - }, - http: { - verb: 'POST', - path: '/deleteNotification' + if (instance) { + notificationFk = instance.notificationFk; + workerId = instance.userFk; + } else { + const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id); + notificationFk = notificationSubscription.notificationFk; + workerId = notificationSubscription.userFk; } - }); - Self.deleteNotification = async function(ctx, notificationId) { - const models = Self.app.models; - const user = ctx.req.accessToken.userId; - const modifiedUser = await getUserToModify(notificationId, null, models); + const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']}); + const available = await Self.getAvailable(workerId); + const hasAcl = available.has(notificationFk); - if (user != modifiedUser.id && user != modifiedUser.bossFk) - throw new UserError('You dont have permission to modify this user'); - - await models.NotificationSubscription.destroyById(notificationId); - }; - - async function getUserToModify(notificationId, userFk, models) { - let userToModify = userFk; - if (notificationId) { - const subscription = await models.NotificationSubscription.findById(notificationId); - userToModify = subscription.userFk; - } - return await models.Worker.findOne({ - fields: ['id', 'bossFk'], - where: { - id: userToModify - } - }); + if (!hasAcl || (userId != worker.id && userId != worker.bossFk)) + throw new UserError('The notification subscription of this worker cant be modified'); } + + Self.getAvailable = async function(userId, options) { + const availableNotificationsMap = new Map(); + const models = Self.app.models; + + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const roles = await models.RoleMapping.find({ + fields: ['roleId'], + where: {principalId: userId} + }, myOptions); + + const availableNotifications = await models.NotificationAcl.find({ + fields: ['notificationFk', 'roleFk'], + include: {relation: 'notification'}, + where: { + roleFk: { + inq: roles.map(role => role.roleId), + }, + } + }, myOptions); + + for (available of availableNotifications) { + availableNotificationsMap.set(available.notificationFk, { + id: null, + notificationFk: available.notificationFk, + name: available.notification().name, + description: available.notification().description, + active: false + }); + } + return availableNotificationsMap; + }; }; diff --git a/back/models/specs/notificationSubscription.spec.js b/back/models/specs/notificationSubscription.spec.js index c7f37abed..c2adcbc59 100644 --- a/back/models/specs/notificationSubscription.spec.js +++ b/back/models/specs/notificationSubscription.spec.js @@ -1,74 +1,126 @@ const models = require('vn-loopback/server/server').models; describe('loopback model NotificationSubscription', () => { - it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { + it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => { const tx = await models.NotificationSubscription.beginTransaction({}); + let error; try { - const options = {transaction: tx}; - const user = 9; + const options = {transaction: tx, accessToken: {userId: 9}}; + await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error.message).toEqual('The notification subscription of this worker cant be modified'); + }); + + it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + let error; + + try { + const options = {transaction: tx, accessToken: {userId: 1}}; + await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error.message).toEqual('The notification subscription of this worker cant be modified'); + }); + + it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + let error; + + try { + const options = {transaction: tx, accessToken: {userId: 9}}; const notificationSubscriptionId = 2; - const ctx = {req: {accessToken: {userId: user}}}; - const notification = await models.NotificationSubscription.findById(notificationSubscriptionId); + await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options); - let error; - - try { - await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); - } catch (e) { - error = e; - } - - expect(error.message).toContain('You dont have permission to modify this user'); await tx.rollback(); } catch (e) { await tx.rollback(); - throw e; + error = e; } + + expect(error.message).toEqual('The notification subscription of this worker cant be modified'); }); - it('Should delete a notification if the user is editing itself', async() => { + it('should add a notification subscription if the user is editing itself', async() => { const tx = await models.NotificationSubscription.beginTransaction({}); + let error; try { - const options = {transaction: tx}; - const user = 9; + const options = {transaction: tx, accessToken: {userId: 9}}; + await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toBeUndefined(); + }); + + it('should delete a notification subscription if the user is editing itself', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + let error; + + try { + const options = {transaction: tx, accessToken: {userId: 9}}; + const notificationSubscriptionId = 6; + await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toBeUndefined(); + }); + + it('should add a notification subscription if the user is editing a subordinate', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + let error; + + try { + const options = {transaction: tx, accessToken: {userId: 9}}; + await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toBeUndefined(); + }); + + it('should delete a notification subscription if the user is editing a subordinate', async() => { + const tx = await models.NotificationSubscription.beginTransaction({}); + let error; + + try { + const options = {transaction: tx, accessToken: {userId: 19}}; const notificationSubscriptionId = 4; - const ctx = {req: {accessToken: {userId: user}}}; - const notification = await models.NotificationSubscription.findById(notificationSubscriptionId); + await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options); - await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); - - const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId); - - expect(deletedNotification).toBeNull(); await tx.rollback(); } catch (e) { await tx.rollback(); - throw e; + error = e; } - }); - it('Should delete a notification if the user is editing a subordinate', async() => { - const tx = await models.NotificationSubscription.beginTransaction({}); - - try { - const options = {transaction: tx}; - const user = 9; - const notificationSubscriptionId = 5; - const ctx = {req: {accessToken: {userId: user}}}; - const notification = await models.NotificationSubscription.findById(notificationSubscriptionId); - - await models.NotificationSubscription.deleteNotification(ctx, notification.id, options); - - const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId); - - expect(deletedNotification).toBeNull(); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + expect(error).toBeUndefined(); }); }); diff --git a/back/models/specs/vn-user.spec.js b/back/models/specs/vn-user.spec.js index 3700b919a..8689a7854 100644 --- a/back/models/specs/vn-user.spec.js +++ b/back/models/specs/vn-user.spec.js @@ -1,4 +1,5 @@ const models = require('vn-loopback/server/server').models; +const ForbiddenError = require('vn-loopback/util/forbiddenError'); describe('loopback model VnUser', () => { it('should return true if the user has the given role', async() => { @@ -12,4 +13,42 @@ describe('loopback model VnUser', () => { expect(result).toBeFalsy(); }); + + describe('userSecurity', () => { + const itManagementId = 115; + const hrId = 37; + const employeeId = 1; + + it('should check if you are the same user', async() => { + const ctx = {options: {accessToken: {userId: employeeId}}}; + await models.VnUser.userSecurity(ctx, employeeId); + }); + + it('should check for higher privileges', async() => { + const ctx = {options: {accessToken: {userId: itManagementId}}}; + await models.VnUser.userSecurity(ctx, employeeId); + }); + + it('should check if you have medium privileges and the user email is not verified', async() => { + const ctx = {options: {accessToken: {userId: hrId}}}; + await models.VnUser.userSecurity(ctx, employeeId); + }); + + it('should throw an error if you have medium privileges and the users email is verified', async() => { + const tx = await models.VnUser.beginTransaction({}); + const ctx = {options: {accessToken: {userId: hrId}}}; + try { + const options = {transaction: tx}; + const userToUpdate = await models.VnUser.findById(1, null, options); + userToUpdate.updateAttribute('emailVerified', 1, options); + + await models.VnUser.userSecurity(ctx, employeeId, options); + await tx.rollback(); + } catch (error) { + await tx.rollback(); + + expect(error).toEqual(new ForbiddenError()); + } + }); + }); }); diff --git a/back/models/url.js b/back/models/url.js new file mode 100644 index 000000000..b603e9eb5 --- /dev/null +++ b/back/models/url.js @@ -0,0 +1,4 @@ +module.exports = Self => { + require('../methods/url/getByUser')(Self); + require('../methods/url/getUrl')(Self); +}; diff --git a/back/models/vn-user.js b/back/models/vn-user.js index cf210b61b..de5bf7b63 100644 --- a/back/models/vn-user.js +++ b/back/models/vn-user.js @@ -1,6 +1,7 @@ const vnModel = require('vn-loopback/common/models/vn-model'); -const LoopBackContext = require('loopback-context'); const {Email} = require('vn-print'); +const ForbiddenError = require('vn-loopback/util/forbiddenError'); +const LoopBackContext = require('loopback-context'); module.exports = function(Self) { vnModel(Self); @@ -12,6 +13,7 @@ module.exports = function(Self) { require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/renew-token')(Self); + require('../methods/vn-user/update-user')(Self); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create'); @@ -90,11 +92,7 @@ module.exports = function(Self) { }; Self.on('resetPasswordRequest', async function(info) { - const loopBackContext = LoopBackContext.getCurrentContext(); - const httpCtx = {req: loopBackContext.active}; - const httpRequest = httpCtx.req.http.req; - const headers = httpRequest.headers; - const origin = headers.origin; + const url = await Self.app.models.Url.getUrl(); const defaultHash = '/reset-password?access_token=$token$'; const recoverHashes = { @@ -110,7 +108,7 @@ module.exports = function(Self) { const params = { recipient: info.email, lang: user.lang, - url: origin + '/#!' + recoverHash + url: url.slice(0, -1) + recoverHash }; const options = Object.assign({}, info.options); @@ -178,45 +176,75 @@ module.exports = function(Self) { Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls .filter(acl => acl.property != 'changePassword'); - // FIXME: https://redmine.verdnatura.es/issues/5761 - // Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { - // if (!ctx.args || !ctx.args.data.email) return; + Self.userSecurity = async(ctx, userId, options) => { + const models = Self.app.models; + const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken; + const ctxToken = {req: {accessToken}}; - // const loopBackContext = LoopBackContext.getCurrentContext(); - // const httpCtx = {req: loopBackContext.active}; - // const httpRequest = httpCtx.req.http.req; - // const headers = httpRequest.headers; - // const origin = headers.origin; - // const url = origin.split(':'); + if (userId === accessToken.userId) return; - // class Mailer { - // async send(verifyOptions, cb) { - // const params = { - // url: verifyOptions.verifyHref, - // recipient: verifyOptions.to, - // lang: ctx.req.getLocale() - // }; + const myOptions = {}; + if (typeof options == 'object') + Object.assign(myOptions, options); - // const email = new Email('email-verify', params); - // email.send(); + const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions); + if (hasHigherPrivileges) return; - // cb(null, verifyOptions.to); - // } - // } + const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions); + const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions); + if (!user.emailVerified && hasMediumPrivileges) return; - // const options = { - // type: 'email', - // to: instance.email, - // from: {}, - // redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`, - // template: false, - // mailer: new Mailer, - // host: url[1].split('/')[2], - // port: url[2], - // protocol: url[0], - // user: Self - // }; + throw new ForbiddenError(); + }; - // await instance.verify(options); - // }); + Self.observe('after save', async ctx => { + const instance = ctx?.instance; + const newEmail = instance?.email; + const oldEmail = ctx?.hookState?.oldInstance?.email; + if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return; + + const loopBackContext = LoopBackContext.getCurrentContext(); + const httpCtx = {req: loopBackContext.active}; + const httpRequest = httpCtx.req.http.req; + const headers = httpRequest.headers; + const origin = headers.origin; + const url = origin.split(':'); + + const env = process.env.NODE_ENV; + const liliumUrl = await Self.app.models.Url.findOne({ + where: {and: [ + {appName: 'lilium'}, + {environment: env} + ]} + }); + + class Mailer { + async send(verifyOptions, cb) { + const params = { + url: verifyOptions.verifyHref, + recipient: verifyOptions.to + }; + + const email = new Email('email-verify', params); + email.send(); + + cb(null, verifyOptions.to); + } + } + + const options = { + type: 'email', + to: newEmail, + from: {}, + redirect: `${liliumUrl.url}verifyEmail?userId=${instance.id}`, + template: false, + mailer: new Mailer, + host: url[1].split('/')[2], + port: url[2], + protocol: url[0], + user: Self + }; + + await instance.verify(options, ctx.options); + }); }; diff --git a/back/models/vn-user.json b/back/models/vn-user.json index 9e3f8df89..0f6daff5a 100644 --- a/back/models/vn-user.json +++ b/back/models/vn-user.json @@ -13,19 +13,12 @@ "type": "number", "id": true }, - "name": { + "name": { "type": "string", "required": true }, "username": { - "type": "string", - "mysql": { - "columnName": "name" - } - }, - "password": { - "type": "string", - "required": true + "type": "string" }, "roleFk": { "type": "number", @@ -45,6 +38,9 @@ "email": { "type": "string" }, + "emailVerified": { + "type": "boolean" + }, "created": { "type": "date" }, @@ -144,7 +140,8 @@ "image", "hasGrant", "realm", - "email" + "email", + "emailVerified" ] } } diff --git a/db/changes/231201/00-ACL.sql b/db/.archive/231201/00-ACL.sql similarity index 100% rename from db/changes/231201/00-ACL.sql rename to db/.archive/231201/00-ACL.sql diff --git a/db/changes/231201/00-chatRefactor.sql b/db/.archive/231201/00-chatRefactor.sql similarity index 100% rename from db/changes/231201/00-chatRefactor.sql rename to db/.archive/231201/00-chatRefactor.sql diff --git a/db/changes/231201/00-invoiceInSerial.sql b/db/.archive/231201/00-invoiceInSerial.sql similarity index 100% rename from db/changes/231201/00-invoiceInSerial.sql rename to db/.archive/231201/00-invoiceInSerial.sql diff --git a/db/changes/231201/00-itemType_isFragile.sql b/db/.archive/231201/00-itemType_isFragile.sql similarity index 100% rename from db/changes/231201/00-itemType_isFragile.sql rename to db/.archive/231201/00-itemType_isFragile.sql diff --git a/db/changes/231201/00-mailACL.sql b/db/.archive/231201/00-mailACL.sql similarity index 100% rename from db/changes/231201/00-mailACL.sql rename to db/.archive/231201/00-mailACL.sql diff --git a/db/changes/231201/00-operator.sql b/db/.archive/231201/00-operator.sql similarity index 100% rename from db/changes/231201/00-operator.sql rename to db/.archive/231201/00-operator.sql diff --git a/db/changes/231201/00-supplierAccount_deleteTriggers.sql b/db/.archive/231201/00-supplierAccount_deleteTriggers.sql similarity index 100% rename from db/changes/231201/00-supplierAccount_deleteTriggers.sql rename to db/.archive/231201/00-supplierAccount_deleteTriggers.sql diff --git a/db/changes/231201/00-ticket_getWarnings.sql b/db/.archive/231201/00-ticket_getWarnings.sql similarity index 100% rename from db/changes/231201/00-ticket_getWarnings.sql rename to db/.archive/231201/00-ticket_getWarnings.sql diff --git a/db/changes/231201/00-wagon.sql b/db/.archive/231201/00-wagon.sql similarity index 100% rename from db/changes/231201/00-wagon.sql rename to db/.archive/231201/00-wagon.sql diff --git a/db/changes/231202/00-delivery.sql b/db/.archive/231202/00-delivery.sql similarity index 100% rename from db/changes/231202/00-delivery.sql rename to db/.archive/231202/00-delivery.sql diff --git a/db/changes/231203/00-delivery.sql b/db/.archive/231203/00-delivery.sql similarity index 100% rename from db/changes/231203/00-delivery.sql rename to db/.archive/231203/00-delivery.sql diff --git a/db/changes/231204/00-rollbackDelivery.sql b/db/.archive/231204/00-rollbackDelivery.sql similarity index 100% rename from db/changes/231204/00-rollbackDelivery.sql rename to db/.archive/231204/00-rollbackDelivery.sql diff --git a/db/changes/231205/00-printQueueArgs.sql b/db/.archive/231205/00-printQueueArgs.sql similarity index 100% rename from db/changes/231205/00-printQueueArgs.sql rename to db/.archive/231205/00-printQueueArgs.sql diff --git a/db/changes/231401/00-claimBeginningAfterInsert.sql b/db/.archive/231401/00-claimBeginningAfterInsert.sql similarity index 100% rename from db/changes/231401/00-claimBeginningAfterInsert.sql rename to db/.archive/231401/00-claimBeginningAfterInsert.sql diff --git a/db/changes/231401/00-clientBeforeUpdate.sql b/db/.archive/231401/00-clientBeforeUpdate.sql similarity index 100% rename from db/changes/231401/00-clientBeforeUpdate.sql rename to db/.archive/231401/00-clientBeforeUpdate.sql diff --git a/db/changes/231401/00-hotfixDelivery.sql b/db/.archive/231401/00-hotfixDelivery.sql similarity index 100% rename from db/changes/231401/00-hotfixDelivery.sql rename to db/.archive/231401/00-hotfixDelivery.sql diff --git a/db/changes/231401/00-invoiceOutAfterInsert.sql b/db/.archive/231401/00-invoiceOutAfterInsert.sql similarity index 100% rename from db/changes/231401/00-invoiceOutAfterInsert.sql rename to db/.archive/231401/00-invoiceOutAfterInsert.sql diff --git a/db/changes/231401/00-negativeBases.sql b/db/.archive/231401/00-negativeBases.sql similarity index 100% rename from db/changes/231401/00-negativeBases.sql rename to db/.archive/231401/00-negativeBases.sql diff --git a/db/changes/231401/00-workerNotes.sql b/db/.archive/231401/00-workerNotes.sql similarity index 100% rename from db/changes/231401/00-workerNotes.sql rename to db/.archive/231401/00-workerNotes.sql diff --git a/db/changes/231402/00-negativeBases.sql b/db/.archive/231402/00-negativeBases.sql similarity index 100% rename from db/changes/231402/00-negativeBases.sql rename to db/.archive/231402/00-negativeBases.sql diff --git a/db/changes/231801/00-aclClientInforma.sql b/db/.archive/231801/00-aclClientInforma.sql similarity index 100% rename from db/changes/231801/00-aclClientInforma.sql rename to db/.archive/231801/00-aclClientInforma.sql diff --git a/db/changes/231801/00-acl_receiptEmail.sql b/db/.archive/231801/00-acl_receiptEmail.sql similarity index 100% rename from db/changes/231801/00-acl_receiptEmail.sql rename to db/.archive/231801/00-acl_receiptEmail.sql diff --git a/db/changes/231801/00-clientInforma.sql b/db/.archive/231801/00-clientInforma.sql similarity index 100% rename from db/changes/231801/00-clientInforma.sql rename to db/.archive/231801/00-clientInforma.sql diff --git a/db/changes/231801/00-client_setRatingAcl.sql b/db/.archive/231801/00-client_setRatingAcl.sql similarity index 100% rename from db/changes/231801/00-client_setRatingAcl.sql rename to db/.archive/231801/00-client_setRatingAcl.sql diff --git a/db/changes/231801/00-deleteProcs_refund.sql b/db/.archive/231801/00-deleteProcs_refund.sql similarity index 100% rename from db/changes/231801/00-deleteProcs_refund.sql rename to db/.archive/231801/00-deleteProcs_refund.sql diff --git a/db/changes/231801/00-deviceProduction.sql b/db/.archive/231801/00-deviceProduction.sql similarity index 100% rename from db/changes/231801/00-deviceProduction.sql rename to db/.archive/231801/00-deviceProduction.sql diff --git a/db/changes/231801/00-kkearEntryNotes.sql b/db/.archive/231801/00-kkearEntryNotes.sql similarity index 100% rename from db/changes/231801/00-kkearEntryNotes.sql rename to db/.archive/231801/00-kkearEntryNotes.sql diff --git a/db/changes/231801/00-newCompanyI18n.sql b/db/.archive/231801/00-newCompanyI18n.sql similarity index 100% rename from db/changes/231801/00-newCompanyI18n.sql rename to db/.archive/231801/00-newCompanyI18n.sql diff --git a/db/changes/231801/00-newTableWeb.sql b/db/.archive/231801/00-newTableWeb.sql similarity index 100% rename from db/changes/231801/00-newTableWeb.sql rename to db/.archive/231801/00-newTableWeb.sql diff --git a/db/changes/231801/00-observationEmailACL.sql b/db/.archive/231801/00-observationEmailACL.sql similarity index 100% rename from db/changes/231801/00-observationEmailACL.sql rename to db/.archive/231801/00-observationEmailACL.sql diff --git a/db/changes/231801/00-optimiceZoneEstimatedDelivery.sql b/db/.archive/231801/00-optimiceZoneEstimatedDelivery.sql similarity index 100% rename from db/changes/231801/00-optimiceZoneEstimatedDelivery.sql rename to db/.archive/231801/00-optimiceZoneEstimatedDelivery.sql diff --git a/db/changes/231801/00-saleTracking.sql b/db/.archive/231801/00-saleTracking.sql similarity index 100% rename from db/changes/231801/00-saleTracking.sql rename to db/.archive/231801/00-saleTracking.sql diff --git a/db/changes/231801/00-ticketConfig.sql b/db/.archive/231801/00-ticketConfig.sql similarity index 100% rename from db/changes/231801/00-ticketConfig.sql rename to db/.archive/231801/00-ticketConfig.sql diff --git a/db/changes/231801/00-updateIsVies.sql b/db/.archive/231801/00-updateIsVies.sql similarity index 100% rename from db/changes/231801/00-updateIsVies.sql rename to db/.archive/231801/00-updateIsVies.sql diff --git a/db/changes/231801/00-updateisViesClient.sql b/db/.archive/231801/00-updateisViesClient.sql similarity index 100% rename from db/changes/231801/00-updateisViesClient.sql rename to db/.archive/231801/00-updateisViesClient.sql diff --git a/db/changes/231801/00-userAcl.sql b/db/.archive/231801/00-userAcl.sql similarity index 100% rename from db/changes/231801/00-userAcl.sql rename to db/.archive/231801/00-userAcl.sql diff --git a/db/changes/231801/00-userRoleLog.sql b/db/.archive/231801/00-userRoleLog.sql similarity index 100% rename from db/changes/231801/00-userRoleLog.sql rename to db/.archive/231801/00-userRoleLog.sql diff --git a/db/changes/231801/01-viewCompany10L.sql b/db/.archive/231801/01-viewCompany10L.sql similarity index 100% rename from db/changes/231801/01-viewCompany10L.sql rename to db/.archive/231801/01-viewCompany10L.sql diff --git a/db/changes/232001/00-clientWorkerName.sql b/db/.archive/232001/00-clientWorkerName.sql similarity index 100% rename from db/changes/232001/00-clientWorkerName.sql rename to db/.archive/232001/00-clientWorkerName.sql diff --git a/db/changes/232001/00-createWorker.sql b/db/.archive/232001/00-createWorker.sql similarity index 100% rename from db/changes/232001/00-createWorker.sql rename to db/.archive/232001/00-createWorker.sql diff --git a/db/changes/232001/00-invoiceOut_new.sql b/db/.archive/232001/00-invoiceOut_new.sql similarity index 100% rename from db/changes/232001/00-invoiceOut_new.sql rename to db/.archive/232001/00-invoiceOut_new.sql diff --git a/db/changes/232001/00-wagon.sql b/db/.archive/232001/00-wagon.sql similarity index 100% rename from db/changes/232001/00-wagon.sql rename to db/.archive/232001/00-wagon.sql diff --git a/db/changes/232201/00-defaulterView.sql b/db/.archive/232201/00-defaulterView.sql similarity index 100% rename from db/changes/232201/00-defaulterView.sql rename to db/.archive/232201/00-defaulterView.sql diff --git a/db/changes/232201/00-procedurecanAdvance.sql b/db/.archive/232201/00-procedurecanAdvance.sql similarity index 100% rename from db/changes/232201/00-procedurecanAdvance.sql rename to db/.archive/232201/00-procedurecanAdvance.sql diff --git a/db/changes/232201/00-procedurecanbePostponed.sql b/db/.archive/232201/00-procedurecanbePostponed.sql similarity index 100% rename from db/changes/232201/00-procedurecanbePostponed.sql rename to db/.archive/232201/00-procedurecanbePostponed.sql diff --git a/db/changes/232201/00-workerConfigPayMethod.sql b/db/.archive/232201/00-workerConfigPayMethod.sql similarity index 100% rename from db/changes/232201/00-workerConfigPayMethod.sql rename to db/.archive/232201/00-workerConfigPayMethod.sql diff --git a/db/changes/232202/00-procedurecanAdvance.sql b/db/.archive/232202/00-procedurecanAdvance.sql similarity index 100% rename from db/changes/232202/00-procedurecanAdvance.sql rename to db/.archive/232202/00-procedurecanAdvance.sql diff --git a/db/changes/232202/00-procedurecanbePostponed.sql b/db/.archive/232202/00-procedurecanbePostponed.sql similarity index 100% rename from db/changes/232202/00-procedurecanbePostponed.sql rename to db/.archive/232202/00-procedurecanbePostponed.sql diff --git a/db/changes/232401/.gitkeep b/db/.archive/232401/.gitkeep similarity index 100% rename from db/changes/232401/.gitkeep rename to db/.archive/232401/.gitkeep diff --git a/db/changes/232401/00-buyConfig_travelConfig.sql b/db/.archive/232401/00-buyConfig_travelConfig.sql similarity index 100% rename from db/changes/232401/00-buyConfig_travelConfig.sql rename to db/.archive/232401/00-buyConfig_travelConfig.sql diff --git a/db/changes/232401/00-printer.sql b/db/.archive/232401/00-printer.sql similarity index 100% rename from db/changes/232401/00-printer.sql rename to db/.archive/232401/00-printer.sql diff --git a/db/changes/232401/00-ticket_warehouse.sql b/db/.archive/232401/00-ticket_warehouse.sql similarity index 100% rename from db/changes/232401/00-ticket_warehouse.sql rename to db/.archive/232401/00-ticket_warehouse.sql diff --git a/db/changes/232401/00-userPassExpired.sql b/db/.archive/232401/00-userPassExpired.sql similarity index 100% rename from db/changes/232401/00-userPassExpired.sql rename to db/.archive/232401/00-userPassExpired.sql diff --git a/db/changes/232402/00-hotFix_travelConfig.sql b/db/.archive/232402/00-hotFix_travelConfig.sql similarity index 100% rename from db/changes/232402/00-hotFix_travelConfig.sql rename to db/.archive/232402/00-hotFix_travelConfig.sql diff --git a/db/changes/232601/00-aclAccount.sql b/db/.archive/232601/00-aclAccount.sql similarity index 100% rename from db/changes/232601/00-aclAccount.sql rename to db/.archive/232601/00-aclAccount.sql diff --git a/db/changes/232601/00-aclInvoiceTickets.sql b/db/.archive/232601/00-aclInvoiceTickets.sql similarity index 100% rename from db/changes/232601/00-aclInvoiceTickets.sql rename to db/.archive/232601/00-aclInvoiceTickets.sql diff --git a/db/changes/232601/00-aclMailAliasAccount.sql b/db/.archive/232601/00-aclMailAliasAccount.sql similarity index 100% rename from db/changes/232601/00-aclMailAliasAccount.sql rename to db/.archive/232601/00-aclMailAliasAccount.sql diff --git a/db/changes/232601/00-aclMailForward.sql b/db/.archive/232601/00-aclMailForward.sql similarity index 100% rename from db/changes/232601/00-aclMailForward.sql rename to db/.archive/232601/00-aclMailForward.sql diff --git a/db/changes/232601/00-aclRole.sql b/db/.archive/232601/00-aclRole.sql similarity index 100% rename from db/changes/232601/00-aclRole.sql rename to db/.archive/232601/00-aclRole.sql diff --git a/db/changes/232601/00-aclVnUser.sql b/db/.archive/232601/00-aclVnUser.sql similarity index 100% rename from db/changes/232601/00-aclVnUser.sql rename to db/.archive/232601/00-aclVnUser.sql diff --git a/db/changes/232601/00-aclVnUser_renewToken.sql b/db/.archive/232601/00-aclVnUser_renewToken.sql similarity index 100% rename from db/changes/232601/00-aclVnUser_renewToken.sql rename to db/.archive/232601/00-aclVnUser_renewToken.sql diff --git a/db/changes/232601/00-entry_updateComission.sql b/db/.archive/232601/00-entry_updateComission.sql similarity index 100% rename from db/changes/232601/00-entry_updateComission.sql rename to db/.archive/232601/00-entry_updateComission.sql diff --git a/db/changes/232601/00-packingSiteAdvanced.sql b/db/.archive/232601/00-packingSiteAdvanced.sql similarity index 100% rename from db/changes/232601/00-packingSiteAdvanced.sql rename to db/.archive/232601/00-packingSiteAdvanced.sql diff --git a/db/changes/232601/00-salix.sql b/db/.archive/232601/00-salix.sql similarity index 100% rename from db/changes/232601/00-salix.sql rename to db/.archive/232601/00-salix.sql diff --git a/db/changes/232601/00-useSpecificsAcls.sql b/db/.archive/232601/00-useSpecificsAcls.sql similarity index 100% rename from db/changes/232601/00-useSpecificsAcls.sql rename to db/.archive/232601/00-useSpecificsAcls.sql diff --git a/db/changes/232601/01-invoiceOutPdf.sql b/db/.archive/232601/01-invoiceOutPdf.sql similarity index 100% rename from db/changes/232601/01-invoiceOutPdf.sql rename to db/.archive/232601/01-invoiceOutPdf.sql diff --git a/db/changes/232602/01-aclAddAlias.sql b/db/.archive/232602/01-aclAddAlias.sql similarity index 100% rename from db/changes/232602/01-aclAddAlias.sql rename to db/.archive/232602/01-aclAddAlias.sql diff --git a/db/changes/232801/00-authCode.sql b/db/.archive/232801/00-authCode.sql similarity index 100% rename from db/changes/232801/00-authCode.sql rename to db/.archive/232801/00-authCode.sql diff --git a/db/changes/232801/00-client_create.sql b/db/.archive/232801/00-client_create.sql similarity index 100% rename from db/changes/232801/00-client_create.sql rename to db/.archive/232801/00-client_create.sql diff --git a/db/changes/232801/00-client_create2.sql b/db/.archive/232801/00-client_create2.sql similarity index 100% rename from db/changes/232801/00-client_create2.sql rename to db/.archive/232801/00-client_create2.sql diff --git a/db/changes/232801/00-department.sql b/db/.archive/232801/00-department.sql similarity index 100% rename from db/changes/232801/00-department.sql rename to db/.archive/232801/00-department.sql diff --git a/db/changes/232801/00-fix_editCredit.sql b/db/.archive/232801/00-fix_editCredit.sql similarity index 100% rename from db/changes/232801/00-fix_editCredit.sql rename to db/.archive/232801/00-fix_editCredit.sql diff --git a/db/changes/232801/00-user.sql b/db/.archive/232801/00-user.sql similarity index 100% rename from db/changes/232801/00-user.sql rename to db/.archive/232801/00-user.sql diff --git a/db/changes/232802/01-aclWorkerDisable.sql b/db/.archive/232802/01-aclWorkerDisable.sql similarity index 100% rename from db/changes/232802/01-aclWorkerDisable.sql rename to db/.archive/232802/01-aclWorkerDisable.sql diff --git a/db/changes/234003/00-ticket_canAdvance_zone.sql b/db/changes/234003/00-ticket_canAdvance_zone.sql new file mode 100644 index 000000000..ee07ce978 --- /dev/null +++ b/db/changes/234003/00-ticket_canAdvance_zone.sql @@ -0,0 +1,133 @@ +DELIMITER $$ +$$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT) +BEGIN +/** + * Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar. + * + * @param vDateFuture Fecha de los tickets que se quieren adelantar. + * @param vDateToAdvance Fecha a cuando se quiere adelantar. + * @param vWarehouseFk Almacén + */ + DECLARE vDateInventory DATE; + + SELECT inventoried INTO vDateInventory FROM config; + + DROP TEMPORARY TABLE IF EXISTS tmp.stock; + CREATE TEMPORARY TABLE tmp.stock + (itemFk INT PRIMARY KEY, + amount INT) + ENGINE = MEMORY; + + INSERT INTO tmp.stock(itemFk, amount) + SELECT itemFk, SUM(quantity) amount FROM + ( + SELECT itemFk, quantity + FROM itemTicketOut + WHERE shipped >= vDateInventory + AND shipped < vDateFuture + AND warehouseFk = vWarehouseFk + UNION ALL + SELECT itemFk, quantity + FROM itemEntryIn + WHERE landed >= vDateInventory + AND landed < vDateFuture + AND isVirtualStock = FALSE + AND warehouseInFk = vWarehouseFk + UNION ALL + SELECT itemFk, quantity + FROM itemEntryOut + WHERE shipped >= vDateInventory + AND shipped < vDateFuture + AND warehouseOutFk = vWarehouseFk + ) t + GROUP BY itemFk HAVING amount != 0; + + CREATE OR REPLACE TEMPORARY TABLE tmp.filter + (INDEX (id)) + SELECT + origin.ticketFk futureId, + dest.ticketFk id, + dest.state, + origin.futureState, + origin.futureIpt, + dest.ipt, + origin.workerFk, + origin.futureLiters, + origin.futureLines, + dest.shipped, + origin.shipped futureShipped, + dest.totalWithVat, + origin.totalWithVat futureTotalWithVat, + dest.agency, + origin.futureAgency, + dest.lines, + dest.liters, + origin.futureLines - origin.hasStock AS notMovableLines, + (origin.futureLines = origin.hasStock) AS isFullMovable, + origin.futureZoneFk, + origin.futureZoneName, + origin.classColor futureClassColor, + dest.classColor + FROM ( + SELECT + s.ticketFk, + c.salesPersonFk workerFk, + t.shipped, + t.totalWithVat, + st.name futureState, + t.addressFk, + am.name futureAgency, + count(s.id) futureLines, + GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt, + CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters, + SUM((s.quantity <= IFNULL(st.amount,0))) hasStock, + z.id futureZoneFk, + z.name futureZoneName, + st.classColor + FROM ticket t + JOIN client c ON c.id = t.clientFk + JOIN sale s ON s.ticketFk = t.id + JOIN saleVolume sv ON sv.saleFk = s.id + JOIN item i ON i.id = s.itemFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN state st ON st.id = ts.stateFk + JOIN agencyMode am ON t.agencyModeFk = am.id + JOIN zone z ON t.zoneFk = z.id + LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk + LEFT JOIN tmp.stock st ON st.itemFk = i.id + WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture) + AND t.warehouseFk = vWarehouseFk + GROUP BY t.id + ) origin + JOIN ( + SELECT + t.id ticketFk, + t.addressFk, + st.name state, + GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt, + t.shipped, + t.totalWithVat, + am.name agency, + CAST(SUM(litros) AS DECIMAL(10,0)) liters, + CAST(COUNT(*) AS DECIMAL(10,0)) `lines`, + st.classColor + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN saleVolume sv ON sv.saleFk = s.id + JOIN item i ON i.id = s.itemFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN state st ON st.id = ts.stateFk + JOIN agencyMode am ON t.agencyModeFk = am.id + LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk + WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance) + AND t.warehouseFk = vWarehouseFk + AND st.order <= 5 + GROUP BY t.id + ) dest ON dest.addressFk = origin.addressFk + WHERE origin.hasStock != 0; + + DROP TEMPORARY TABLE tmp.stock; +END$$ +DELIMITER ; + diff --git a/db/changes/234004/.gitkeep b/db/changes/234004/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/db/changes/234004/00-workerTimeControl.sql b/db/changes/234004/00-workerTimeControl.sql new file mode 100644 index 000000000..8f6263533 --- /dev/null +++ b/db/changes/234004/00-workerTimeControl.sql @@ -0,0 +1,41 @@ +UPDATE `vn`.`workerTimeControlConfig` + SET `timeToBreakTime` = 18000; + +ALTER TABLE `vn`.`workerTimeControlConfig` + DROP COLUMN IF EXISTS `maxTimeToBreak`; +ALTER TABLE `vn`.`workerTimeControlConfig` + ADD COLUMN maxTimeToBreak INT DEFAULT 3600 NULL; + +ALTER TABLE `vn`.`workerTimeControlConfig` + DROP COLUMN IF EXISTS `maxWorkShortCycle`; + +ALTER TABLE `vn`.`workerTimeControlConfig` + ADD COLUMN `maxWorkShortCycle` INT(10) UNSIGNED DEFAULT 561600 + COMMENT 'Máximo tiempo que un trabajador puede estar trabajando con el que adquirirá el derecho a un descanso semanal corto'; + +ALTER TABLE `vn`.`workerTimeControlConfig` + DROP COLUMN IF EXISTS `maxWorkLongCycle`; + +ALTER TABLE `vn`.`workerTimeControlConfig` + ADD COLUMN `maxWorkLongCycle` INT(10) UNSIGNED DEFAULT 950400 + COMMENT 'Máximo tiempo que un trabajador puede estar trabajando con el que adquirirá el derecho a un descanso semanal largo'; + +CREATE TABLE IF NOT EXISTS `vn`.`workerTimeControlError` ( + `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, + `code` char(35) NOT NULL, + `description` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `code` (`code`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +INSERT IGNORE INTO `vn`.`workerTimeControlError` (`code`, `description`) + VALUES + ('IS_NOT_ALLOWED_FUTURE', 'No se permite fichar a futuro'), + ('INACTIVE_BUSINESS', 'No hay un contrato en vigor'), + ('IS_NOT_ALLOWED_WORK', 'No está permitido trabajar'), + ('ODD_WORKERTIMECONTROL', 'Fichadas impares'), + ('DAY_MAX_TIME', 'Superado el tiempo máximo entre entrada y salida'), + ('BREAK_DAY', 'Descanso diario'), + ('BREAK_WEEK', 'Descanso semanal'), + ('WRONG_DIRECTION', 'Dirección incorrecta'), + ('UNDEFINED_ERROR', 'Error sin definir'); \ No newline at end of file diff --git a/db/changes/234004/01-timeControl_calculate.sql b/db/changes/234004/01-timeControl_calculate.sql new file mode 100644 index 000000000..93d88c047 --- /dev/null +++ b/db/changes/234004/01-timeControl_calculate.sql @@ -0,0 +1,194 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`timeControl_calculate`( + vDatedFrom DATETIME, + vDatedTo DATETIME) +BEGIN +/* + * Agrupa por trabajador y día, el tiempo de trabajo y descanso retribuido(si tiene). + * Los registros horarios incorrectos (tmp.timeControlError) no se considerarán. + * Si un trabajador ha trabajado más de un cierto umbral de tiempo (vTimeToBreakTime) + * y no ha tenido descansos que superen un parámetro determinado(vMaxTimeToBreak), + * se le añadirá un tiempo de descanso (vBreakTime) a sus horas trabajadas. + * El tiempo de descanso solo se añade si el trabajador realmente disfrutó del descanso. + * Si disfrutó de menos tiempo de descanso, solo se añade el tiempo que disfrutó. + * + * @param vDatedFrom + * @param vDatedTo + * + * @return tmp.timeControlCalculate + * (workerFk, dated, timeWorkSeconds, timeWorkSexagesimal, timeWorkDecimal, timed) + */ + DECLARE vHourSeconds INTEGER; + DECLARE vDatedFromYesterday DATETIME; + DECLARE vDatedToTomorrow DATETIME; + DECLARE vTimeToBreakTime INT; + DECLARE vBreakTime INT; + DECLARE vMaxTimeToBreak INT; + + SELECT DATE_SUB(vDatedFrom, INTERVAL 1 DAY), DATE_ADD(vDatedTo, INTERVAL 1 DAY) + INTO vDatedFromYesterday, vDatedToTomorrow; + + SELECT timeToBreakTime, breakTime, maxTimeToBreak, TIME_TO_SEC('01:00:00') + INTO vTimeToBreakTime, vBreakTime, vMaxTimeToBreak, vHourSeconds + FROM workerTimeControlConfig + LIMIT 1; + + CALL timeControl_getError(vDatedFromYesterday, vDatedToTomorrow); + + CREATE OR REPLACE TEMPORARY TABLE tmp.workerTimeControl + (INDEX(userFk, timed), INDEX(timed), INDEX(direction)) + ENGINE = MEMORY + SELECT wtc.userFk, + wtc.timed, + DATE(wtc.timed) dated, + wtc.direction, + TRUE isReal + FROM workerTimeControl wtc + JOIN tmp.`user` u ON u.userFk = wtc.userFk + LEFT JOIN ( + SELECT wtc.userFk, MIN(wtc.timed) firstIn + FROM workerTimeControl wtc + JOIN tmp.`user` u ON u.userFk = wtc.userFk + LEFT JOIN tmp.timeControlError tce ON tce.id = wtc.id + WHERE wtc.timed BETWEEN vDatedFromYesterday AND vDatedToTomorrow + AND wtc.direction = 'in' + AND tce.id IS NULL + GROUP BY userFk + ) fi ON wtc.userFk = fi.userFk + LEFT JOIN ( + SELECT wtc.userFk, MAX(wtc.timed) lastOut + FROM workerTimeControl wtc + JOIN tmp.`user` u ON u.userFk = wtc.userFk + LEFT JOIN tmp.timeControlError tce ON tce.id = wtc.id + WHERE wtc.timed BETWEEN vDatedFromYesterday AND vDatedToTomorrow + AND wtc.direction = 'out' + AND tce.id IS NULL + GROUP BY userFk + ) lo ON wtc.userFk = lo.userFk + LEFT JOIN tmp.timeControlError tce ON tce.id = wtc.id + WHERE wtc.timed BETWEEN fi.firstIn AND lo.lastOut + AND tce.id IS NULL + ORDER BY wtc.userFk, wtc.timed; + + CREATE OR REPLACE TEMPORARY TABLE tmp.wtcToinsert + (INDEX(timed)) + ENGINE = MEMORY + WITH wtc AS( + SELECT timed, + userFk, + dated, + direction, + LEAD(dated) OVER + (PARTITION BY userFk, dated ORDER BY timed) nextDay, + LEAD(userFk) OVER + (PARTITION BY userFk ORDER BY timed) nextUserFk, + ROW_NUMBER() OVER (ORDER BY userFk, timed) MOD 2 isOdd + FROM tmp.workerTimeControl + WHERE timed BETWEEN vDatedFromYesterday AND vDatedToTomorrow + ORDER BY userFk, timed + ), wtcToinsert AS( + SELECT userFk, + dated, + IF(userFk = nextUserFk + AND nextDay IS NULL + AND isOdd + AND direction <> 'out', TRUE, FALSE) outNextDay, + IF(userFk = nextUserFk + AND nextDay IS NULL + AND NOT isOdd + AND direction <> 'out', TRUE, FALSE) outNextDayWhitBreak + FROM wtc + HAVING outNextDay OR outNextDayWhitBreak + )SELECT userFk, util.dayEnd(dated) timed, 'out' direction + FROM wtcToinsert + WHERE outNextDay + UNION ALL + SELECT userFk, dated + INTERVAL 1 DAY, 'in' + FROM wtcToinsert + WHERE outNextDay + UNION ALL + SELECT userFk, util.dayEnd(dated) - INTERVAL 1 SECOND, 'middle' + FROM wtcToinsert + WHERE outNextDayWhitBreak + UNION ALL + SELECT userFk, util.dayEnd(dated), 'out' + FROM wtcToinsert + WHERE outNextDayWhitBreak + UNION ALL + SELECT userFk, dated + INTERVAL 1 DAY, 'in' + FROM wtcToinsert + WHERE outNextDayWhitBreak + UNION ALL + SELECT userFk, dated + INTERVAL 1 DAY + INTERVAL 1 SECOND, 'middle' + FROM wtcToinsert + WHERE outNextDayWhitBreak; + + INSERT INTO tmp.workerTimeControl (userFk, timed, dated, direction, isReal) + SELECT userFk, timed, DATE(timed), direction, FALSE + FROM tmp.wtcToinsert; + + SET @accumulatedForBreakTime = 0; + SET @oldrealDay = NULL; + CREATE OR REPLACE TEMPORARY TABLE tmp.timeControlCalculate + WITH workerTimed AS ( + SELECT + userFk, + dated, + timed, + (direction ='in' AND isReal) breakPoint, + SUM(CASE WHEN (direction ='in' AND isReal) THEN TRUE ELSE FALSE END) + OVER (ORDER BY userFk, timed) AS realDay, + TIMESTAMPDIFF(SECOND, LAG(timed) + OVER (PARTITION BY userFk, dated ORDER BY timed), timed) gapTime, + ROW_NUMBER() + OVER (PARTITION BY userFk, dated ORDER BY timed) MOD 2 isOdd + FROM tmp.workerTimeControl + WHERE timed BETWEEN vDatedFromYesterday AND vDatedToTomorrow + ), accumulated AS ( + SELECT SUM(IF(isOdd, 0, gapTime)) + OVER (PARTITION BY userFk,dated ORDER BY userFk,timed) accumulatedWorkTime, + SUM(IF(NOT isOdd OR breakPoint, 0, IFNULL(gapTime, 0))) + OVER (PARTITION BY realDay ORDER BY realDay,timed) accumulatedBreakTime, + IF(realDay <> @oldrealDay OR (isOdd AND gapTime >= vMaxTimeToBreak), + @accumulatedForBreakTime := 0, + @accumulatedForBreakTime := @accumulatedForBreakTime + + IF(isOdd, 0, gapTime )) accumulatedForBreakTime, + @oldrealDay := realDay, + userFk, + dated, + realDay + FROM workerTimed + ), totalWorked AS ( + SELECT userFk, + dated, + MAX(accumulatedWorkTime) + + IF(MAX(accumulatedForBreakTime) >= vTimeToBreakTime, + LEAST(vBreakTime, MAX(accumulatedBreakTime)), + 0) timeWorkSeconds + FROM accumulated + GROUP BY userFk, dated + )SELECT tw.userFk, + tw.dated, + timeWorkSeconds, + SEC_TO_TIME(timeWorkSeconds) timeWorkSexagesimal, + timeWorkSeconds / vHourSeconds timeWorkDecimal, + sub.tableTimed + FROM totalWorked tw + JOIN ( + SELECT userFk, + dated, + GROUP_CONCAT(DATE_FORMAT(timed, "%H:%i") ORDER BY timed ASC + SEPARATOR ' - ')tableTimed + FROM tmp.workerTimeControl + WHERE timed BETWEEN vDatedFromYesterday AND vDatedToTomorrow + AND isReal + GROUP BY userFk, dated + )sub ON sub.dated = tw.dated + AND sub.userFk = tw.userFk + WHERE tw.dated BETWEEN vDatedFrom AND vDatedTo; + + DROP TEMPORARY TABLE tmp.timeControlError; + DROP TEMPORARY TABLE tmp.wtcToinsert; + DROP TEMPORARY TABLE tmp.workerTimeControl; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/changes/234004/02-workerTimeControl_clockIn.sql b/db/changes/234004/02-workerTimeControl_clockIn.sql new file mode 100644 index 000000000..69091e51c --- /dev/null +++ b/db/changes/234004/02-workerTimeControl_clockIn.sql @@ -0,0 +1,286 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`workerTimeControl_clockIn`( + vWorkerFk INT, + vTimed DATETIME, + vDirection VARCHAR(10) +) +BEGIN +/** + * Verifica si el empleado puede fichar + * @param vWorkerFk Identificador del trabajador + * @param vTimed valor de la fichada, IF vTimed IS NULL vTimed = NOW + * @param vDirection solo se pueden pasa los valores del campo + * workerTimeControl.direction ENUM('in', 'out', 'middle') + * @return Si todo es correcto, retorna el número de id la tabla workerTimeControl. + * Si hay algún problema, devuelve el mesaje que se debe mostrar al usuario + * Solo retorna el primer problema, en caso de no ocurrir ningún error se añadirá + * fichada a la tabla vn.workerTimeControl + */ + + DECLARE vLastIn DATETIME; + DECLARE vLastOut DATETIME; + DECLARE vNextIn DATETIME; + DECLARE vNextOut DATETIME; + DECLARE vNextDirection ENUM('in', 'out'); + DECLARE vLastDirection ENUM('in', 'out'); + DECLARE vDayMaxTime INTEGER; + DECLARE vDayBreak INT; + DECLARE vShortWeekBreak INT; + DECLARE vLongWeekBreak INT; + DECLARE vWeekScope INT; + DECLARE vMailTo VARCHAR(50) DEFAULT NULL; + DECLARE vUserName VARCHAR(50) DEFAULT NULL; + DECLARE vIsError BOOLEAN DEFAULT FALSE; + DECLARE vErrorMessage VARCHAR(255) DEFAULT NULL; + DECLARE vErrorCode VARCHAR(50); + DECLARE vDated DATE; + DECLARE vIsAllowedToWork VARCHAR(50); + DECLARE vIsManual BOOLEAN DEFAULT TRUE; + DECLARE vMaxWorkShortCycle INT; + DECLARE vMaxWorkLongCycle INT; + + DECLARE EXIT HANDLER FOR SQLSTATE '45000' + BEGIN + + SELECT CONCAT(u.name, '@verdnatura.es'), + CONCAT(w.firstName, ' ', w.lastName) + INTO vMailTo, vUserName + FROM account.user u + JOIN worker w ON w.bossFk = u.id + WHERE w.id = vWorkerFk; + + SELECT `description` INTO vErrorMessage + FROM workerTimeControlError + WHERE `code` = vErrorCode; + + IF vErrorMessage IS NULL THEN + SET vErrorMessage = 'Error sin definir'; + END IF; + + SELECT vErrorMessage `error`; + SELECT CONCAT(vUserName, + ' no ha podido fichar por el siguiente problema: ', + vErrorMessage) + INTO vErrorMessage; + + CALL mail_insert( vMailTo, vMailTo, 'Error al fichar', vErrorMessage); + END; + + IF (vTimed IS NULL) THEN + SET vTimed = util.VN_NOW(); + SET vIsManual = FALSE; + END IF; + + SET vDated = DATE(vTimed); + + SELECT IF(pc.name = 'Conductor +3500kg', + wc.dayBreakDriver, + wc.dayBreak), + wc.shortWeekBreak, + wc.longWeekBreak, + wc.weekScope, + wc.dayMaxTime, + wc.maxWorkShortCycle, + wc.maxWorkLongCycle + INTO vDayBreak, + vShortWeekBreak, + vLongWeekBreak, + vWeekScope, + vDayMaxTime, + vMaxWorkShortCycle, + vMaxWorkLongCycle + FROM business b + JOIN professionalCategory pc + ON pc.id = b.workerBusinessProfessionalCategoryFk + JOIN workerTimeControlConfig wc + WHERE b.workerFk = vWorkerFk + AND vDated BETWEEN b.started AND IFNULL(b.ended, vDated); + + -- CONTRATO EN VIGOR + IF vDayBreak IS NULL THEN + SET vErrorCode = 'INACTIVE_BUSINESS'; + CALL util.throw(vErrorCode); + END IF; + + -- FICHADAS A FUTURO + IF vTimed > util.VN_NOW() + INTERVAL 1 MINUTE THEN + SET vErrorCode = 'IS_NOT_ALLOWED_FUTURE'; + CALL util.throw(vErrorCode); + END IF; + + -- VERIFICAR SI ESTÁ PERMITIDO TRABAJAR + CALL timeBusiness_calculateByWorker(vWorkerFk, vDated, vDated); + SELECT isAllowedToWork INTO vIsAllowedToWork + FROM tmp.timeBusinessCalculate; + DROP TEMPORARY TABLE tmp.timeBusinessCalculate; + + IF NOT vIsAllowedToWork THEN + SET vErrorCode = 'IS_NOT_ALLOWED_WORK'; + CALL util.throw(vErrorCode); + END IF; + + -- DIRECCION CORRECTA + CALL workerTimeControl_direction(vWorkerFk, vTimed); + IF (SELECT + IF(IF(option1 IN ('inMiddle', 'outMiddle'), + 'middle', + option1) <> vDirection + AND IF(option2 IN ('inMiddle', 'outMiddle'), + 'middle', + IFNULL(option2, '')) <> vDirection, + TRUE , + FALSE) + FROM tmp.workerTimeControlDirection + ) THEN + SET vIsError = TRUE; + END IF; + + DROP TEMPORARY TABLE tmp.workerTimeControlDirection; + IF vIsError THEN + SET vErrorCode = 'WRONG_DIRECTION'; + CALL util.throw(vErrorCode); + END IF; + + -- FICHADAS IMPARES + SELECT timed INTO vLastIn + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction = 'in' + AND timed < vTimed + ORDER BY timed DESC + LIMIT 1; + + IF (SELECT IF(vDirection = 'in', + MOD(COUNT(*), 2) , + IF (vDirection = 'out', NOT MOD(COUNT(*), 2), FALSE)) + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND timed BETWEEN vLastIn AND vTimed + ) THEN + SET vErrorCode = 'ODD_WORKERTIMECONTROL'; + CALL util.throw(vErrorCode); + END IF; + + -- DESCANSO DIARIO + SELECT timed INTO vLastOut + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction = 'out' + AND timed < vTimed + ORDER BY timed DESC + LIMIT 1; + + SELECT timed INTO vNextIn + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction = 'in' + AND timed > vTimed + ORDER BY timed ASC + LIMIT 1; + + CASE vDirection + WHEN 'in' THEN + IF UNIX_TIMESTAMP(vTimed) - UNIX_TIMESTAMP(vLastOut) <= vDayBreak THEN + SET vIsError = TRUE; + END IF; + WHEN 'out' THEN + IF UNIX_TIMESTAMP(vNextIn) - UNIX_TIMESTAMP(vTimed) <= vDayBreak THEN + SET vIsError = TRUE; + END IF; + ELSE BEGIN END; + END CASE; + + IF vIsError THEN + SET vErrorCode = 'BREAK_DAY'; + CALL util.throw(vErrorCode); + END IF; + + + + IF (vDirection IN('in', 'out')) THEN + -- VERIFICA MAXIMO TIEMPO DESDE ENTRADA HASTA LA SALIDA + + SELECT timed INTO vNextOut + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction = 'out' + AND timed > vTimed + ORDER BY timed ASC + LIMIT 1; + + SELECT direction INTO vNextDirection + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction IN('in','out') + AND timed > vTimed + ORDER BY timed ASC + LIMIT 1; + + SELECT direction INTO vLastDirection + FROM workerTimeControl + WHERE userFk = vWorkerFk + AND direction IN('in', 'out') + AND timed < vTimed + ORDER BY timed ASC + LIMIT 1; + + IF (vDirection ='in' + AND vNextDirection = 'out' + AND UNIX_TIMESTAMP(vNextOut) - UNIX_TIMESTAMP(vTimed) > vDayMaxTime) OR + (vDirection ='out' + AND vLastDirection = 'in' + AND UNIX_TIMESTAMP(vTimed) -UNIX_TIMESTAMP(vLastIn) > vDayMaxTime) THEN + SET vErrorCode = 'DAY_MAX_TIME'; + CALL util.throw(vErrorCode); + END IF; + + -- VERIFICA DESCANSO SEMANAL + + WITH wtc AS( + (SELECT timed + FROM vn.workerTimeControl + WHERE userFk = vWorkerFk + AND direction IN ('in', 'out') + AND timed BETWEEN vTimed - INTERVAL (vWeekScope * 2) SECOND + AND vTimed + INTERVAL (vWeekScope * 2) SECOND ) + UNION + (SELECT vTimed) + ), wtcGap AS( + SELECT timed, + TIMESTAMPDIFF(SECOND, LAG(timed) OVER (ORDER BY timed), timed) gap + FROM wtc + ORDER BY timed + ), wtcBreak AS( + SELECT timed, + IF(IFNULL(gap, 0) > vShortWeekBreak, TRUE, FALSE) hasShortBreak, + IF(IFNULL(gap, 0) > vLongWeekBreak, TRUE, FALSE) hasLongBreak + FROM wtcGap + ORDER BY timed + ), wtcBreakCounter AS( + SELECT timed, + SUM(hasShortBreak) OVER (ORDER BY timed) breakCounter , + LEAD(hasLongBreak) OVER (ORDER BY timed) nextHasLongBreak + FROM wtcBreak + )SELECT TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed)) > vMaxWorkLongCycle OR + (TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed))> vMaxWorkShortCycle + AND NOT SUM(IFNULL(nextHasLongBreak, 1))) + hasError INTO vIsError + FROM wtcBreakCounter + GROUP BY breakCounter + HAVING hasError + LIMIT 1; + + IF vIsError THEN + SET vErrorCode = 'BREAK_WEEK'; + CALL util.throw(vErrorCode); + END IF; + END IF; + + -- SE PERMITE FICHAR + INSERT INTO workerTimeControl(userFk, timed, direction, `manual`) + VALUES(vWorkerFk, vTimed, vDirection, vIsManual); + + SELECT LAST_INSERT_ID() id; + +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/changes/234201/00-ACL_workerDepartment.sql b/db/changes/234201/00-ACL_workerDepartment.sql new file mode 100644 index 000000000..ceb8d5d75 --- /dev/null +++ b/db/changes/234201/00-ACL_workerDepartment.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES + ('WorkerDepartment', '*', '*', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/234201/00-account_acl.sql b/db/changes/234201/00-account_acl.sql new file mode 100644 index 000000000..8dfe1d1ec --- /dev/null +++ b/db/changes/234201/00-account_acl.sql @@ -0,0 +1,7 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES + ('VnUser', 'higherPrivileges', '*', 'ALLOW', 'ROLE', 'itManagement'), + ('VnUser', 'mediumPrivileges', '*', 'ALLOW', 'ROLE', 'hr'), + ('VnUser', 'updateUser', '*', 'ALLOW', 'ROLE', 'employee'); + +ALTER TABLE `account`.`user` ADD `username` varchar(30) AS (name) VIRTUAL; diff --git a/db/changes/234201/00-aclClient.sql b/db/changes/234201/00-aclClient.sql new file mode 100644 index 000000000..3df9522cf --- /dev/null +++ b/db/changes/234201/00-aclClient.sql @@ -0,0 +1,3 @@ + +INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) + VALUES('TicketCollection', '*', 'WRITE', 'ALLOW', 'ROLE', 'production'); diff --git a/db/changes/234201/00-aclSetPassword.sql b/db/changes/234201/00-aclSetPassword.sql new file mode 100644 index 000000000..44b3e9de0 --- /dev/null +++ b/db/changes/234201/00-aclSetPassword.sql @@ -0,0 +1,4 @@ +INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId) + VALUES ('Worker','setPassword','*','ALLOW','ROLE','employee'); + + diff --git a/db/changes/234201/00-aclUrlHedera.sql b/db/changes/234201/00-aclUrlHedera.sql new file mode 100644 index 000000000..79d9fb4c8 --- /dev/null +++ b/db/changes/234201/00-aclUrlHedera.sql @@ -0,0 +1,7 @@ +INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) + VALUES + ('hedera', 'test', 'https://test-shop.verdnatura.es/'), + ('hedera', 'production', 'https://shop.verdnatura.es/'); + +INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) + VALUES('Url', 'getByUser', 'READ', 'ALLOW', 'ROLE', '$everyone'); diff --git a/db/changes/234201/00-dropWorkerCreate.sql b/db/changes/234201/00-dropWorkerCreate.sql new file mode 100644 index 000000000..b903909b6 --- /dev/null +++ b/db/changes/234201/00-dropWorkerCreate.sql @@ -0,0 +1 @@ +DROP PROCEDURE IF EXISTS `vn`.`workerCreate`; diff --git a/db/changes/234201/00-packagingFk.sql b/db/changes/234201/00-packagingFk.sql new file mode 100644 index 000000000..9a775ada9 --- /dev/null +++ b/db/changes/234201/00-packagingFk.sql @@ -0,0 +1,5 @@ +ALTER TABLE `vn`.`buy` CHANGE `packageFk` `packagingFk` varchar(10) +CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '--' NULL; + +ALTER TABLE `vn`.`buy` +ADD COLUMN `packageFk` varchar(10) AS (`packagingFk`) VIRTUAL; \ No newline at end of file diff --git a/db/changes/234201/00-packagingFkviews.sql b/db/changes/234201/00-packagingFkviews.sql new file mode 100644 index 000000000..49d41c26c --- /dev/null +++ b/db/changes/234201/00-packagingFkviews.sql @@ -0,0 +1,150 @@ +CREATE SCHEMA IF NOT EXISTS `vn2008`; + +CREATE OR REPLACE DEFINER=`root`@`localhost` + SQL SECURITY DEFINER + VIEW `vn`.`awbVolume` +AS SELECT `d`.`awbFk` AS `awbFk`, + `b`.`stickers` * `i`.`density` * IF( + `p`.`volume` > 0, + `p`.`volume`, + `p`.`width` * `p`.`depth` * IF(`p`.`height` = 0, `i`.`size` + 10, `p`.`height`) + ) / (`vc`.`aerealVolumetricDensity` * 1000) AS `volume`, + `b`.`id` AS `buyFk` +FROM ( + ( + ( + ( + ( + ( + ( + ( + `vn`.`buy` `b` + JOIN `vn`.`item` `i` ON(`b`.`itemFk` = `i`.`id`) + ) + JOIN `vn`.`itemType` `it` ON(`i`.`typeFk` = `it`.`id`) + ) + JOIN `vn`.`packaging` `p` ON(`p`.`id` = `b`.`packagingFk`) + ) + JOIN `vn`.`entry` `e` ON(`b`.`entryFk` = `e`.`id`) + ) + JOIN `vn`.`travel` `t` ON(`t`.`id` = `e`.`travelFk`) + ) + JOIN `vn`.`duaEntry` `de` ON(`de`.`entryFk` = `e`.`id`) + ) + JOIN `vn`.`dua` `d` ON(`d`.`id` = `de`.`duaFk`) + ) + JOIN `vn`.`volumeConfig` `vc` + ) +WHERE `t`.`shipped` > makedate(year(`util`.`VN_CURDATE`()) - 1, 1); + + +CREATE OR REPLACE DEFINER=`root`@`localhost` + SQL SECURITY DEFINER + VIEW `vn2008`.`Compres` +AS SELECT `c`.`id` AS `Id_Compra`, + `c`.`entryFk` AS `Id_Entrada`, + `c`.`itemFk` AS `Id_Article`, + `c`.`buyingValue` AS `Costefijo`, + `c`.`quantity` AS `Cantidad`, + `c`.`packagingFk` AS `Id_Cubo`, + `c`.`stickers` AS `Etiquetas`, + `c`.`freightValue` AS `Portefijo`, + `c`.`packageValue` AS `Embalajefijo`, + `c`.`comissionValue` AS `Comisionfija`, + `c`.`packing` AS `Packing`, + `c`.`grouping` AS `grouping`, + `c`.`groupingMode` AS `caja`, + `c`.`location` AS `Nicho`, + `c`.`price1` AS `Tarifa1`, + `c`.`price2` AS `Tarifa2`, + `c`.`price3` AS `Tarifa3`, + `c`.`minPrice` AS `PVP`, + `c`.`printedStickers` AS `Vida`, + `c`.`isChecked` AS `punteo`, + `c`.`ektFk` AS `buy_edi_id`, + `c`.`created` AS `odbc_date`, + `c`.`isIgnored` AS `Novincular`, + `c`.`isPickedOff` AS `isPickedOff`, + `c`.`workerFk` AS `Id_Trabajador`, + `c`.`weight` AS `weight`, + `c`.`dispatched` AS `dispatched`, + `c`.`containerFk` AS `container_id`, + `c`.`itemOriginalFk` AS `itemOriginalFk` +FROM `vn`.`buy` `c`; + +CREATE OR REPLACE DEFINER=`root`@`localhost` + SQL SECURITY DEFINER + VIEW `vn2008`.`buySource` +AS SELECT `b`.`entryFk` AS `Id_Entrada`, + `b`.`isPickedOff` AS `isPickedOff`, + NULL AS `tarifa0`, + `e`.`kop` AS `kop`, + `b`.`id` AS `Id_Compra`, + `i`.`typeFk` AS `tipo_id`, + `b`.`itemFk` AS `Id_Article`, + `i`.`size` AS `Medida`, + `i`.`stems` AS `Tallos`, + `b`.`stickers` AS `Etiquetas`, + `b`.`packagingFk` AS `Id_Cubo`, + `b`.`buyingValue` AS `Costefijo`, + `b`.`packing` AS `Packing`, + `b`.`grouping` AS `Grouping`, + `b`.`quantity` AS `Cantidad`, + `b`.`price2` AS `Tarifa2`, + `b`.`price3` AS `Tarifa3`, + `b`.`isChecked` AS `Punteo`, + `b`.`groupingMode` AS `Caja`, + `i`.`isToPrint` AS `Imprimir`, + `i`.`name` AS `Article`, + `vn`.`ink`.`picture` AS `Tinta`, + `i`.`originFk` AS `id_origen`, + `i`.`minPrice` AS `PVP`, + NULL AS `Id_Accion`, + `s`.`company_name` AS `pro`, + `i`.`hasMinPrice` AS `Min`, + `b`.`isIgnored` AS `Novincular`, + `b`.`freightValue` AS `Portefijo`, + round(`b`.`buyingValue` * `b`.`quantity`, 2) AS `Importe`, + `b`.`printedStickers` AS `Vida`, + `i`.`comment` AS `reference`, + `b`.`workerFk` AS `Id_Trabajador`, + `e`.`s1` AS `S1`, + `e`.`s2` AS `S2`, + `e`.`s3` AS `S3`, + `e`.`s4` AS `S4`, + `e`.`s5` AS `S5`, + `e`.`s6` AS `S6`, + 0 AS `price_fixed`, + `i`.`producerFk` AS `producer_id`, + `i`.`subName` AS `tag1`, + `i`.`value5` AS `tag2`, + `i`.`value6` AS `tag3`, + `i`.`value7` AS `tag4`, + `i`.`value8` AS `tag5`, + `i`.`value9` AS `tag6`, + `s`.`company_name` AS `company_name`, + `b`.`weight` AS `weightPacking`, + `i`.`packingOut` AS `packingOut`, + `b`.`itemOriginalFk` AS `itemOriginalFk`, + `io`.`longName` AS `itemOriginalName`, + `it`.`gramsMax` AS `gramsMax` +FROM ( + ( + ( + ( + ( + ( + `vn`.`item` `i` + JOIN `vn`.`itemType` `it` ON(`it`.`id` = `i`.`typeFk`) + ) + LEFT JOIN `vn`.`ink` ON(`vn`.`ink`.`id` = `i`.`inkFk`) + ) + LEFT JOIN `vn`.`buy` `b` ON(`b`.`itemFk` = `i`.`id`) + ) + LEFT JOIN `vn`.`item` `io` ON(`io`.`id` = `b`.`itemOriginalFk`) + ) + LEFT JOIN `edi`.`ekt` `e` ON(`e`.`id` = `b`.`ektFk`) + ) + LEFT JOIN `edi`.`supplier` `s` ON(`e`.`pro` = `s`.`supplier_id`) + ); + diff --git a/db/changes/234201/00-zoneIncluded.sql b/db/changes/234201/00-zoneIncluded.sql new file mode 100644 index 000000000..12d4058cf --- /dev/null +++ b/db/changes/234201/00-zoneIncluded.sql @@ -0,0 +1,26 @@ +ALTER TABLE `vn`.`zoneIncluded` + ADD COLUMN `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT FIRST, + DROP PRIMARY KEY, + DROP FOREIGN KEY `zoneFk2`, + DROP FOREIGN KEY `zoneGeoFk2`, + DROP KEY `geoFk_idx`, + ADD PRIMARY KEY (`id`), + ADD CONSTRAINT `zoneIncluded_FK_1` FOREIGN KEY (zoneFk) REFERENCES `vn`.`zone`(id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT `zoneIncluded_FK_2` FOREIGN KEY (geoFk) REFERENCES `vn`.`zoneGeo`(id) ON DELETE CASCADE ON UPDATE CASCADE, + ADD CONSTRAINT `unique_zone_geo` UNIQUE (`zoneFk`, `geoFk`); + +DROP TRIGGER IF EXISTS `vn`.`zoneIncluded_afterDelete`; +USE `vn`; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`zoneIncluded_afterDelete` + AFTER DELETE ON `zoneIncluded` + FOR EACH ROW +BEGIN + INSERT INTO zoneLog + SET `action` = 'delete', + `changedModel` = 'zoneIncluded', + `changedModelId` = OLD.zoneFk, + `userFk` = account.myUser_getId(); +END$$ +DELIMITER ; diff --git a/db/changes/234201/02-packagingFktrigger.sql b/db/changes/234201/02-packagingFktrigger.sql new file mode 100644 index 000000000..4edcd3f3e --- /dev/null +++ b/db/changes/234201/02-packagingFktrigger.sql @@ -0,0 +1,57 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`buy_afterUpdate` + AFTER UPDATE ON `buy` + FOR EACH ROW +trig: BEGIN + DECLARE vLanded DATE; + DECLARE vBuyerFk INT; + DECLARE vIsBuyerToBeEmailed BOOL; + DECLARE vItemName VARCHAR(50); + + IF @isModeInventory OR @isTriggerDisabled THEN + LEAVE trig; + END IF; + + IF !(NEW.id <=> OLD.id) + OR !(NEW.entryFk <=> OLD.entryFk) + OR !(NEW.itemFk <=> OLD.itemFk) + OR !(NEW.quantity <=> OLD.quantity) + OR !(NEW.created <=> OLD.created) THEN + CALL stock.log_add('buy', NEW.id, OLD.id); + END IF; + + CALL buy_afterUpsert(NEW.id); + + SELECT w.isBuyerToBeEmailed, t.landed + INTO vIsBuyerToBeEmailed, vLanded + FROM entry e + JOIN travel t ON t.id = e.travelFk + JOIN warehouse w ON w.id = t.warehouseInFk + WHERE e.id = NEW.entryFk; + + SELECT it.workerFk, i.longName + INTO vBuyerFk, vItemName + FROM itemCategory k + JOIN itemType it ON it.categoryFk = k.id + JOIN item i ON i.typeFk = it.id + WHERE i.id = OLD.itemFk; + + IF vIsBuyerToBeEmailed AND + vBuyerFk != account.myUser_getId() AND + vLanded = util.VN_CURDATE() THEN + IF !(NEW.itemFk <=> OLD.itemFk) OR + !(NEW.quantity <=> OLD.quantity) OR + !(NEW.packing <=> OLD.packing) OR + !(NEW.grouping <=> OLD.grouping) OR + !(NEW.packagingFk <=> OLD.packagingFk) OR + !(NEW.weight <=> OLD.weight) THEN + CALL vn.mail_insert( + CONCAT(account.user_getNameFromId(vBuyerFk),'@verdnatura.es'), + CONCAT(account.myUser_getName(),'@verdnatura.es'), + CONCAT('E ', NEW.entryFk ,' Se ha modificado item ', NEW.itemFk, ' ', vItemName), + 'Este email se ha generado automáticamente' + ); + END IF; + END IF; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/db/changes/234201/03-packagingFkProc.sql b/db/changes/234201/03-packagingFkProc.sql new file mode 100644 index 000000000..4876c270e --- /dev/null +++ b/db/changes/234201/03-packagingFkProc.sql @@ -0,0 +1,1381 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`travelVolume`(vTravelFk INT) +BEGIN + + SELECT w1.name AS ORI, + w2.name AS DES, + tr.shipped shipment, + tr.landed landing, + a.name Agencia, + s.name Proveedor, + e.id Id_Entrada, + e.invoiceNumber Referencia, + CAST(ROUND(SUM(GREATEST(b.stickers ,b.quantity /b.packing ) * + vn.item_getVolume(b.itemFk ,b.packagingFk)) / vc.trolleyM3 / 1000000 ,1) AS DECIMAL(10,2)) AS CC, + CAST(ROUND(SUM(GREATEST(b.stickers ,b.quantity /b.packing ) * + vn.item_getVolume(b.itemFk ,b.packagingFk)) / vc.palletM3 / 1000000,1) AS DECIMAL(10,2)) AS espais + FROM vn.buy b + JOIN vn.entry e ON e.id = b.entryFk + JOIN vn.supplier s ON s.id = e.supplierFk + JOIN vn.travel tr ON tr.id = e.travelFk + JOIN vn.agencyMode a ON a.id = tr.agencyModeFk + JOIN vn.warehouse w1 ON w1.id = tr.warehouseInFk + JOIN vn.warehouse w2 ON w2.id = tr.warehouseOutFk + JOIN vn.volumeConfig vc + JOIN vn.item i ON i.id = b.itemFk + JOIN vn.itemType it ON it.id = i.typeFk + WHERE tr.id = vTravelFk; + +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`travelVolume_get`(vFromDated DATE, vToDated DATE, vWarehouseFk INT) +BEGIN + SELECT tr.landed Fecha, + a.name Agencia, + count(DISTINCT e.id) numEntradas, + FLOOR(sum(item_getVolume(b.itemFk, b.packagingFk) * b.stickers / 1000000 )) AS m3 + FROM vn.travel tr + JOIN vn.agencyMode a ON a.id = tr.agencyModeFk + JOIN vn.entry e ON e.travelFk = tr.id + JOIN vn.buy b ON b.entryFk = e.id + WHERE tr.landed BETWEEN vFromDated AND vToDated + AND e.isRaid = FALSE + AND tr.warehouseInFk = vWarehouseFk + GROUP BY tr.landed , a.name ; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`travel_getEntriesMissingPackage`(vSelf INT) +BEGIN + DECLARE vpackageOrPackingNull INT; + DECLARE vTravelFk INT; + + SELECT travelfk INTO vTravelFk + FROM entry + WHERE id = vSelf; + + SELECT e.id entryFk + FROM travel t + JOIN entry e ON e.travelFk = t.id + JOIN buy b ON b.entryFk = e.id + WHERE t.id = vTravelFk + AND (b.packing IS NULL OR b.packagingFk IS NULL); +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketBoxesView`(IN vTicketFk INT) +BEGIN + + SELECT s.id, + s.itemFk, + s.concept, + floor(s.quantity / b.packing) as Cajas, + b.packing, + s.isPicked, + i.size + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN item i ON i.id = s.itemFk + JOIN cache.last_buy lb on lb.warehouse_id = t.warehouseFk AND lb.item_id = s.itemFk + JOIN buy b on b.id = lb.buy_id + JOIN packaging p on p.id = b.packagingFk + WHERE s.quantity >= b.packing + AND t.id = vTicketFk + AND p.isBox + GROUP BY s.itemFk; + + +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`stockBuyedByWorker`( + vDated DATE, + vWorker INT +) +BEGIN +/** + * Inserta el volumen de compra de un comprador + * en stockBuyed de acuerdo con la fecha. + * + * @param vDated Fecha de compra + * @param vWorker Id de trabajador + */ + CREATE OR REPLACE TEMPORARY TABLE tStockBuyed + (INDEX (userFk)) + ENGINE = MEMORY + SELECT requested, reserved, userFk + FROM stockBuyed + WHERE dated = vDated + AND userFk = vWorker; + + DELETE FROM stockBuyed + WHERE dated = vDated + AND userFk = vWorker; + + CALL stockTraslation(vDated); + + INSERT INTO stockBuyed(userFk, buyed, `dated`, reserved, requested, description) + SELECT it.workerFk, + SUM((ti.quantity / b.packing) * buy_getVolume(b.id)) / vc.palletM3 / 1000000, + vDated, + sb.reserved, + sb.requested, + u.name + FROM itemType it + JOIN item i ON i.typeFk = it.id + LEFT JOIN tmp.item ti ON ti.itemFk = i.id + JOIN itemCategory ic ON ic.id = it.categoryFk + JOIN warehouse wh ON wh.code = 'VNH' + JOIN tmp.buyUltimate bu ON bu.itemFk = i.id + AND bu.warehouseFk = wh.id + JOIN buy b ON b.id = bu.buyFk + JOIN volumeConfig vc + JOIN account.`user` u ON u.id = it.workerFk + LEFT JOIN tStockBuyed sb ON sb.userFk = it.workerFk + WHERE ic.display + AND it.workerFk = vWorker; + + SELECT b.entryFk Id_Entrada, + i.id Id_Article, + i.name Article, + ti.quantity Cantidad, + (ac.conversionCoefficient * (ti.quantity / b.packing) * buy_getVolume(b.id)) + / (vc.trolleyM3 * 1000000) buyed, + b.packagingFk id_cubo, + b.packing + FROM tmp.item ti + JOIN item i ON i.id = ti.itemFk + JOIN itemType it ON i.typeFk = it.id + JOIN itemCategory ic ON ic.id = it.categoryFk + JOIN worker w ON w.id = it.workerFk + JOIN auctionConfig ac + JOIN tmp.buyUltimate bu ON bu.itemFk = i.id + AND bu.warehouseFk = ac.warehouseFk + JOIN buy b ON b.id = bu.buyFk + JOIN volumeConfig vc + WHERE ic.display + AND w.id = vWorker; + + DROP TEMPORARY TABLE tmp.buyUltimate, + tmp.item, + tStockBuyed; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`itemShelvingMakeFromDate`(IN `vShelvingFk` VARCHAR(8), IN `vBarcode` VARCHAR(22), IN `vQuantity` INT, IN `vPackagingFk` VARCHAR(10), IN `vGrouping` INT, IN `vPacking` INT, IN `vWarehouseFk` INT, `vCreated` VARCHAR(22)) +BEGIN + + DECLARE vItemFk INT; + + SELECT vn.barcodeToItem(vBarcode) INTO vItemFk; + + SELECT itemFk INTO vItemFk + FROM vn.buy b + WHERE b.id = vItemFk; + + IF (SELECT COUNT(*) FROM vn.shelving WHERE code = vShelvingFk COLLATE utf8_unicode_ci) = 0 THEN + + INSERT IGNORE INTO vn.parking(`code`) VALUES(vShelvingFk); + INSERT INTO vn.shelving(`code`, parkingFk) + SELECT vShelvingFk, id + FROM vn.parking + WHERE `code` = vShelvingFk COLLATE utf8_unicode_ci; + + END IF; + + IF (SELECT COUNT(*) FROM vn.itemShelving + WHERE shelvingFk COLLATE utf8_unicode_ci = vShelvingFk + AND itemFk = vItemFk + AND packing = vPacking) = 1 THEN + + UPDATE vn.itemShelving + SET visible = visible+vQuantity, + created = vCreated + WHERE shelvingFk COLLATE utf8_unicode_ci = vShelvingFk + AND itemFk = vItemFk + AND packing = vPacking; + + ELSE + CALL cache.last_buy_refresh(FALSE); + INSERT INTO itemShelving( itemFk, + shelvingFk, + visible, + created, + `grouping`, + packing, + packagingFk) + SELECT vItemFk, + vShelvingFk, + vQuantity, + vCreated, + IF(vGrouping = 0, IFNULL(b.packing, vPacking), vGrouping) `grouping`, + IF(vPacking = 0, b.packing, vPacking) packing, + IF(vPackagingFk = '', b.packagingFk, vPackagingFk) packaging + FROM vn.item i + LEFT JOIN cache.last_buy lb ON i.id = lb.item_id AND lb.warehouse_id = vWarehouseFk + LEFT JOIN vn.buy b ON b.id = lb.buy_id + WHERE i.id = vItemFk; + END IF; + +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`itemShelving_add`(IN vShelvingFk VARCHAR(8), IN vBarcode VARCHAR(22), IN vQuantity INT, IN vPackagingFk VARCHAR(10), IN vGrouping INT, IN vPacking INT, IN vWarehouseFk INT) +BEGIN + + +/** + * Añade registro o lo actualiza si ya existe. + * + * @param vShelvingFk matrícula del carro + * @param vBarcode el id del registro + * @param vQuantity indica la cantidad del producto + * @param vPackagingFk el packaging del producto en itemShelving, NULL para coger el de la ultima compra + * @param vGrouping el grouping del producto en itemShelving, NULL para coger el de la ultima compra + * @param vPacking el packing del producto, NULL para coger el de la ultima compra + * @param vWarehouseFk indica el sector + * + **/ + + DECLARE vItemFk INT; + + SELECT barcodeToItem(vBarcode) INTO vItemFk; + + IF (SELECT COUNT(*) FROM shelving WHERE code = vShelvingFk COLLATE utf8_unicode_ci) = 0 THEN + + INSERT IGNORE INTO parking(code) VALUES(vShelvingFk); + INSERT INTO shelving(code, parkingFk) + SELECT vShelvingFk, id + FROM parking + WHERE `code` = vShelvingFk COLLATE utf8_unicode_ci; + + END IF; + + IF (SELECT COUNT(*) FROM itemShelving + WHERE shelvingFk COLLATE utf8_unicode_ci = vShelvingFk + AND itemFk = vItemFk + AND packing = vPacking) = 1 THEN + + UPDATE itemShelving + SET visible = visible+vQuantity + WHERE shelvingFk COLLATE utf8_unicode_ci = vShelvingFk AND itemFk = vItemFk AND packing = vPacking; + + ELSE + CALL cache.last_buy_refresh(FALSE); + INSERT INTO itemShelving( itemFk, + shelvingFk, + visible, + grouping, + packing, + packagingFk) + + SELECT vItemFk, + vShelvingFk, + vQuantity, + IFNULL(vGrouping, b.grouping), + IFNULL(vPacking, b.packing), + IFNULL(vPackagingFk, b.packagingFk) + FROM item i + LEFT JOIN cache.last_buy lb ON i.id = lb.item_id AND lb.warehouse_id = vWarehouseFk + LEFT JOIN buy b ON b.id = lb.buy_id + WHERE i.id = vItemFk; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`itemFreight_Show`(vItemFk INT, vWarehouseFk INT) +BEGIN + + SELECT cm3 Volumen_Entrada, + cm3delivery Volumen_Salida, + p.volume Volumen_del_embalaje, + p.width Ancho_del_embalaje, + p.`depth` Largo_del_embalaje, + b.packagingFk , + IFNULL(p.height, i.`size`) + 10 Altura, + b.packing Packing_Entrada, + i.packingOut Packing_Salida, + i.id itemFk, + b.id buyFk, + b.entryFk, + w.name warehouseFk + FROM vn.itemCost ic + JOIN vn.item i ON i.id = ic.itemFk + LEFT JOIN cache.last_buy lb ON lb.item_id = ic.itemFk AND lb.warehouse_id = ic.warehouseFk + LEFT JOIN vn.buy b ON b.id = lb.buy_id + LEFT JOIN vn.packaging p ON p.id = b.packagingFk + LEFT JOIN vn.warehouse w ON w.id = ic.warehouseFk + WHERE ic.itemFk = vItemFk + AND ic.warehouseFk = vWarehouseFk; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`inventoryMake`(vDate DATE, vWh INT) +proc: BEGIN +/** +* Recalcula los inventarios de todos los almacenes, si vWh = 0 +* +* @param vDate Fecha de los nuevos inventarios +* @param vWh almacen al cual hacer el inventario +*/ + + DECLARE vDone BOOL; + DECLARE vEntryFk INT; + DECLARE vTravelFk INT; + DECLARE vDateLastInventory DATE; + DECLARE vDateYesterday DATETIME DEFAULT vDate - INTERVAL 1 SECOND; + DECLARE vWarehouseOutFkInventory INT; + DECLARE vInventorySupplierFk INT; + DECLARE vAgencyModeFkInventory INT; + + DECLARE cWarehouses CURSOR FOR + SELECT id + FROM warehouse + WHERE isInventory + AND vWh IN (0,id); + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + + OPEN cWarehouses; + SET @isModeInventory := TRUE; + l: LOOP + + SET vDone = FALSE; + FETCH cWarehouses INTO vWh; + + IF vDone THEN + LEAVE l; + END IF; + + SELECT w.id INTO vWarehouseOutFkInventory + FROM warehouse w + WHERE w.code = 'inv'; + + SELECT inventorySupplierFk INTO vInventorySupplierFk + FROM entryConfig; + + SELECT am.id INTO vAgencyModeFkInventory + FROM agencyMode am + where code = 'inv'; + + SELECT MAX(landed) INTO vDateLastInventory + FROM travel tr + JOIN entry e ON e.travelFk = tr.id + JOIN buy b ON b.entryFk = e.id + WHERE warehouseOutFk = vWarehouseOutFkInventory + AND landed < vDate + AND e.supplierFk = vInventorySupplierFk + AND warehouseInFk = vWh + AND NOT isRaid; + + IF vDateLastInventory IS NULL THEN + SELECT inventoried INTO vDateLastInventory FROM config; + END IF; + + -- Generamos travel, si no existe. + SET vTravelFK = 0; + + SELECT id INTO vTravelFk + FROM travel + WHERE warehouseOutFk = vWarehouseOutFkInventory + AND warehouseInFk = vWh + AND landed = vDate + AND agencyModeFk = vAgencyModeFkInventory + AND ref = 'inventario' + LIMIT 1; + + IF NOT vTravelFK THEN + + INSERT INTO travel SET + warehouseOutFk = vWarehouseOutFkInventory, + warehouseInFk = vWh, + shipped = vDate, + landed = vDate, + agencyModeFk = vAgencyModeFkInventory, + ref = 'inventario', + isDelivered = TRUE, + isReceived = TRUE; + + SELECT LAST_INSERT_ID() INTO vTravelFk; + + END IF; + + -- Generamos entrada si no existe, o la vaciamos. + SET vEntryFk = 0; + + SELECT id INTO vEntryFk + FROM entry + WHERE supplierFk = vInventorySupplierFk + AND travelFk = vTravelFk; + + IF NOT vEntryFk THEN + + INSERT INTO entry SET + supplierFk = vInventorySupplierFk, + isConfirmed = TRUE, + isOrdered = TRUE, + travelFk = vTravelFk; + + SELECT LAST_INSERT_ID() INTO vEntryFk; + + ELSE + + DELETE FROM buy WHERE entryFk = vEntryFk; + + END IF; + + -- Preparamos tabla auxilar + CREATE OR REPLACE TEMPORARY TABLE tmp.inventory ( + itemFk INT(11) NOT NULL PRIMARY KEY, + quantity int(11) DEFAULT '0', + buyingValue decimal(10,3) DEFAULT '0.000', + freightValue decimal(10,3) DEFAULT '0.000', + packing int(11) DEFAULT '0', + `grouping` smallint(5) unsigned NOT NULL DEFAULT '1', + groupingMode tinyint(4) NOT NULL DEFAULT 0 , + comissionValue decimal(10,3) DEFAULT '0.000', + packageValue decimal(10,3) DEFAULT '0.000', + packageFk varchar(10) COLLATE utf8_unicode_ci DEFAULT '--', + price1 decimal(10,2) DEFAULT '0.00', + price2 decimal(10,2) DEFAULT '0.00', + price3 decimal(10,2) DEFAULT '0.00', + minPrice decimal(10,2) DEFAULT '0.00', + producer varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL, + INDEX (itemFK)) ENGINE = MEMORY; + + -- Compras + INSERT INTO tmp.inventory(itemFk,quantity) + SELECT b.itemFk, SUM(b.quantity) + FROM buy b + JOIN entry e ON e.id = b.entryFk + JOIN travel tr ON tr.id = e.travelFk + WHERE tr.warehouseInFk = vWh + AND tr.landed BETWEEN vDateLastInventory + AND vDateYesterday + AND NOT isRaid + GROUP BY b.itemFk; + SELECT vDateLastInventory , vDateYesterday; + + -- Traslados + INSERT INTO tmp.inventory(itemFk, quantity) + SELECT itemFk, quantityOut + FROM ( + SELECT b.itemFk,- SUM(b.quantity) quantityOut + FROM buy b + JOIN entry e ON e.id = b.entryFk + JOIN travel tr ON tr.id = e.travelFk + WHERE tr.warehouseOutFk = vWh + AND tr.shipped BETWEEN vDateLastInventory + AND vDateYesterday + AND NOT isRaid + GROUP BY b.itemFk + ) sub + ON DUPLICATE KEY UPDATE quantity = IFNULL(quantity, 0) + sub.quantityOut; + + -- Ventas + INSERT INTO tmp.inventory(itemFk,quantity) + SELECT itemFk, saleOut + FROM ( + SELECT s.itemFk, - SUM(s.quantity) saleOut + FROM sale s + JOIN ticket t ON t.id = s.ticketFk + WHERE t.warehouseFk = vWh + AND t.shipped BETWEEN vDateLastInventory AND vDateYesterday + GROUP BY s.itemFk + ) sub + ON DUPLICATE KEY UPDATE quantity = IFNULL(quantity,0) + sub.saleOut; + + -- Actualiza valores de la ultima compra + UPDATE tmp.inventory inv + JOIN cache.last_buy lb ON lb.item_id = inv.itemFk AND lb.warehouse_id = vWh + JOIN buy b ON b.id = lb.buy_id + JOIN item i ON i.id = b.itemFk + LEFT JOIN producer p ON p.id = i.producerFk + SET inv.buyingValue = b.buyingValue, + inv.freightValue = b.freightValue, + inv.packing = b.packing, + inv.`grouping`= b.`grouping`, + inv.groupingMode = b.groupingMode, + inv.comissionValue = b.comissionValue, + inv.packageValue = b.packageValue, + inv.packageFk = b.packagingFk, + inv.price1 = b.price1, + inv.price2 = b.price2, + inv.price3 = b.price3, + inv.minPrice = b.minPrice, + inv.producer = p.name; + + INSERT INTO buy( itemFk, + quantity, + buyingValue, + freightValue, + packing, + `grouping`, + groupingMode, + comissionValue, + packageValue, + packagingFk, + price1, + price2, + price3, + minPrice, + entryFk) + SELECT itemFk, + GREATEST(quantity, 0), + buyingValue, + freightValue, + packing, + `grouping`, + groupingMode, + comissionValue, + packageValue, + packagingFk, + price1, + price2, + price3, + minPrice, + vEntryFk + FROM tmp.inventory; + + SELECT vWh, COUNT(*), util.VN_NOW() FROM tmp.inventory; + + -- Actualizamos el campo lastUsed de item + UPDATE item i + JOIN tmp.inventory i2 ON i2.itemFk = i.id + SET i.lastUsed = NOW() + WHERE i2.quantity; + + -- DROP TEMPORARY TABLE tmp.inventory; + + END LOOP; + + CLOSE cWarehouses; + + UPDATE config SET inventoried = vDate; + SET @isModeInventory := FALSE; + + DROP TEMPORARY TABLE IF EXISTS tmp.entryToDelete; + CREATE TEMPORARY TABLE tmp.entryToDelete + (INDEX(entryId) USING BTREE) ENGINE = MEMORY + SELECT e.id as entryId, + t.id as travelId + FROM travel t + JOIN `entry` e ON e.travelFk = t.id + WHERE e.supplierFk = vInventorySupplierFk + AND t.shipped <= util.VN_CURDATE() - INTERVAL 12 DAY + AND (DAY(t.shipped) <> 1 OR shipped < util.VN_CURDATE() - INTERVAL 12 DAY); + + DELETE e + FROM `entry` e + JOIN tmp.entryToDelete tmp ON tmp.entryId = e.id; + + DELETE IGNORE t + FROM travel t + JOIN tmp.entryToDelete tmp ON tmp.travelId = t.id; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`inventory_repair`() +BEGIN + + DROP TEMPORARY TABLE IF EXISTS tmp.lastEntry; + CREATE TEMPORARY TABLE tmp.lastEntry + (PRIMARY KEY (buyFk)) + SELECT + i.id AS itemFk, + w.id AS warehouseFk, + w.name AS warehouse, + tr.landed, + b.id AS buyFk, + b.entryFk, + b.isIgnored, + b.price2, + b.price3, + b.stickers, + b.packing, + b.grouping, + b.groupingMode, + b.weight, + i.stems, + b.quantity, + b.buyingValue, + b.packagingFk , + s.id AS supplierFk, + s.name AS supplier + FROM itemType it + RIGHT JOIN (entry e + LEFT JOIN supplier s ON s.id = e.supplierFk + RIGHT JOIN buy b ON b.entryFk = e.id + LEFT JOIN item i ON i.id = b.itemFk + LEFT JOIN ink ON ink.id = i.inkFk + LEFT JOIN travel tr ON tr.id = e.travelFk + LEFT JOIN warehouse w ON w.id = tr.warehouseInFk + LEFT JOIN origin o ON o.id = i.originFk + ) ON it.id = i.typeFk + LEFT JOIN edi.ekt ek ON b.ektFk = ek.id + WHERE (b.packagingFk = "--" OR b.price2 = 0 OR b.packing = 0 OR b.buyingValue = 0) AND tr.landed > util.firstDayOfMonth(TIMESTAMPADD(MONTH,-1,util.VN_CURDATE())) AND s.name = 'INVENTARIO'; + + DROP TEMPORARY TABLE IF EXISTS tmp.lastEntryOk; + CREATE TEMPORARY TABLE tmp.lastEntryOk + (PRIMARY KEY (buyFk)) + SELECT + i.id AS itemFk, + w.id AS warehouseFk, + w.name AS warehouse, + tr.landed, + b.id AS buyFk, + b.entryFk, + b.isIgnored, + b.price2, + b.price3, + b.stickers, + b.packing, + b.grouping, + b.groupingMode, + b.weight, + i.stems, + b.quantity, + b.buyingValue, + b.packagingFk, + s.id AS supplierFk, + s.name AS supplier + FROM itemType it + RIGHT JOIN (entry e + LEFT JOIN supplier s ON s.id = e.supplierFk + RIGHT JOIN buy b ON b.entryFk = e.id + LEFT JOIN item i ON i.id = b.itemFk + LEFT JOIN ink ON ink.id = i.inkFk + LEFT JOIN travel tr ON tr.id = e.travelFk + LEFT JOIN warehouse w ON w.id = tr.warehouseInFk + LEFT JOIN origin o ON o.id = i.originFk + ) ON it.id = i.typeFk + LEFT JOIN edi.ekt ek ON b.ektFk = ek.id + WHERE b.packagingFk != "--" AND b.price2 != 0 AND b.packing != 0 AND b.buyingValue > 0 AND tr.landed > util.firstDayOfMonth(TIMESTAMPADD(MONTH,-2,util.VN_CURDATE())) + ORDER BY tr.landed DESC; + + DROP TEMPORARY TABLE IF EXISTS tmp.lastEntryOkGroup; + CREATE TEMPORARY TABLE tmp.lastEntryOkGroup + (INDEX (warehouseFk,itemFk)) + SELECT * + FROM tmp.lastEntryOk tmp + GROUP BY tmp.itemFk,tmp.warehouseFk; + + UPDATE buy b + JOIN tmp.lastEntry lt ON lt.buyFk = b.id + JOIN tmp.lastEntryOkGroup eo ON eo.itemFk = lt.itemFk AND eo.warehouseFk = lt.warehouseFk + SET b.packagingFk = eo.packagingFk WHERE b.packagingFk = "--"; + + UPDATE buy b + JOIN tmp.lastEntry lt ON lt.buyFk = b.id + JOIN tmp.lastEntryOkGroup eo ON eo.itemFk = lt.itemFk AND eo.warehouseFk = lt.warehouseFk + SET b.price2 = eo.price2 WHERE b.price2 = 0 ; + + UPDATE buy b + JOIN tmp.lastEntry lt ON lt.buyFk = b.id + JOIN tmp.lastEntryOkGroup eo ON eo.itemFk = lt.itemFk AND eo.warehouseFk = lt.warehouseFk + SET b.packing = eo.packing WHERE b.packing = 0; + + UPDATE buy b + JOIN tmp.lastEntry lt ON lt.buyFk = b.id + JOIN tmp.lastEntryOkGroup eo ON eo.itemFk = lt.itemFk AND eo.warehouseFk = lt.warehouseFk + SET b.buyingValue = eo.buyingValue WHERE b.buyingValue = 0; + + DROP TEMPORARY TABLE tmp.lastEntry; + DROP TEMPORARY TABLE tmp.lastEntryOk; + DROP TEMPORARY TABLE tmp.lastEntryOkGroup; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`buy_afterUpsert`(vSelf INT) +BEGIN +/** + * Triggered actions when a buy is updated or inserted. + * + * @param vSelf The buy reference + */ + DECLARE vEntryFk INT; + DECLARE vItemFk INT; + DECLARE vPackingOut DECIMAL(10,2); + DECLARE vWarehouse INT; + DECLARE vStandardFlowerBox INT; + DECLARE vWarehouseOut INT; + DECLARE vIsMerchandise BOOL; + DECLARE vIsFeedStock BOOL; + DECLARE vWeight DECIMAL(10,2); + DECLARE vPacking INT; + + SELECT b.entryFk, + b.itemFk, + i.packingOut, + ic.merchandise, + vc.standardFlowerBox, + b.weight, + b.packing + INTO + vEntryFk, + vItemFk, + vPackingOut, + vIsMerchandise, + vStandardFlowerBox, + vWeight, + vPacking + FROM buy b + LEFT JOIN item i ON i.id = b.itemFk + LEFT JOIN itemType it ON it.id = i.typeFk + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN packaging p ON p.id = b.packagingFk AND NOT p.isBox + JOIN volumeConfig vc ON TRUE + WHERE b.id = vSelf; + + SELECT t.warehouseInFk, t.warehouseOutFk + INTO vWarehouse, vWarehouseOut + FROM entry e + JOIN travel t ON t.id = e.travelFk + WHERE e.id = vEntryFk; + + IF vIsMerchandise THEN + + REPLACE itemCost SET + itemFk = vItemFk, + warehouseFk = vWarehouse, + cm3 = buy_getUnitVolume(vSelf), + cm3Delivery = IFNULL((vStandardFlowerBox * 1000) / vPackingOut, buy_getUnitVolume(vSelf)); + + IF vWeight AND vPacking THEN + UPDATE itemCost SET + grams = vWeight * 1000 / vPacking + WHERE itemFk = vItemFk + AND warehouseFk = vWarehouse; + END IF; + END IF; + + SELECT isFeedStock INTO vIsFeedStock + FROM warehouse WHERE id = vWarehouseOut; + + IF vIsFeedStock THEN + INSERT IGNORE INTO producer(`name`) + SELECT es.company_name + FROM buy b + JOIN edi.ekt be ON be.id = b.ektFk + JOIN edi.supplier es ON es.supplier_id = be.pro + WHERE b.id = vSelf; + + END IF; + +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`buy_getUnitVolume`(vSelf INT) + RETURNS int(11) + DETERMINISTIC +BEGIN +/** + * Calculates the unit volume occupied by a buy. + * + * @param vSelf The buy id + * @return The unit volume in cubic centimeters + */ + DECLARE vItem INT; + DECLARE vPackaging VARCHAR(10); + DECLARE vPacking INT; + + SELECT itemFk, packagingFk, packing + INTO vItem, vPackaging, vPacking + FROM buy + WHERE id = vSelf; + + RETURN IFNULL(ROUND(item_getVolume(vItem, vPackaging) / vPacking), 0); +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`buy_recalcPrices`() +BEGIN +/** + * Recalcula los precios para las compras insertadas en tmp.buyRecalc + * + * @param tmp.buyRecalc (id) + */ + DECLARE vLanded DATE; + DECLARE vWarehouseFk INT; + DECLARE vHasNotPrice BOOL; + DECLARE vBuyingValue DECIMAL(10,3); + DECLARE vPackagingFk VARCHAR(10); + DECLARE vIsWarehouseFloramondo BOOL; + + SELECT t.landed, t.warehouseInFk, (w.`name` = 'Floramondo') + INTO vLanded, vWarehouseFk, vIsWarehouseFloramondo + FROM tmp.buyRecalc br + JOIN buy b ON b.id = br.id + JOIN entry e ON e.id = b.entryFk + JOIN travel t ON t.id = e.travelFk + JOIN warehouse w ON w.id = t.warehouseInFk + LIMIT 1; + + CALL rate_getPrices(vLanded, vWarehouseFk); + + UPDATE buy b + JOIN tmp.buyRecalc br ON br.id = b.id AND (@buyId := b.id) + LEFT JOIN packaging p ON p.id = b.packagingFk + JOIN item i ON i.id = b.itemFk + JOIN entry e ON e.id = b.entryFk + JOIN itemType it ON it.id = i.typeFk + JOIN travel tr ON tr.id = e.travelFk + JOIN agencyMode am ON am.id = tr.agencyModeFk + JOIN tmp.rate r + JOIN volumeConfig vc + SET b.freightValue = @PF:= IFNULL(((am.m3 * @m3:= item_getVolume(b.itemFk, b.packagingFk) / 1000000) + / b.packing) * IF(am.hasWeightVolumetric, GREATEST(b.weight / @m3 / vc.aerealVolumetricDensity, 1), 1), 0), + b.comissionValue = @CF:= ROUND(IFNULL(e.commission * b.buyingValue / 100, 0), 3), + b.packageValue = @EF:= IF(vIsWarehouseFloramondo, 0, IFNULL(ROUND(IF(p.isPackageReturnable, p.returnCost / b.packing , p.`value` / b.packing), 3),0)), + b.price3 = @t3:= IF(r.rate3 = 0, b.buyingValue,ROUND((b.buyingValue + @CF + @EF + @PF) / ((100 - r.rate3 - it.promo ) /100) ,2)), -- He añadido que el coste sea igual a tarifa3 si t3 = 0 + b.price2 = @t2:= round(@t3 * (1 + ((r.rate2 - r.rate3)/100)),2), + b.price2 = @t2:= IF(@t2 <= @t3,@t3 , @t2); + + SELECT (b.buyingValue = b.price2), b.buyingValue, b.packagingFk + INTO vHasNotPrice, vBuyingValue, vPackagingFk + FROM vn.buy b + WHERE b.id = @buyId AND b.buyingValue <> 0.01; + + DROP TEMPORARY TABLE tmp.rate; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `edi`.`ekt_load`(IN `vSelf` INT) +proc:BEGIN + + DECLARE vRef INT; + DECLARE vBuy INT; + DECLARE vItem INT; + DECLARE vQty INT; + DECLARE vPackage INT; + DECLARE vPutOrderFk INT; + DECLARE vIsLot BOOLEAN; + DECLARE vForceToPacking INT DEFAULT 2; + DECLARE vEntryFk INT; + DECLARE vHasToChangePackagingFk BOOLEAN; + DECLARE vIsFloramondoDirect BOOLEAN; + DECLARE vTicketFk INT; + DECLARE vHasItemGroup BOOL; + DECLARE vDescription VARCHAR(255); + DECLARE vSaleFk INT; + + -- Carga los datos necesarios del EKT + + SELECT e.ref, qty, package, putOrderFk MOD 1000000, i2.id , NOT ISNULL(eea.addressFk), NOT ISNULL(igto.group_code), + CONCAT(e.`ref`, ' ', e.item, ' ', e.sub, ' EktFk:', e.id) + INTO vRef, vQty, vPackage, vPutOrderFk, vItem, vIsFloramondoDirect, vHasItemGroup, vDescription + FROM edi.ekt e + LEFT JOIN edi.item i ON e.ref = i.id + LEFT JOIN edi.putOrder po ON po.id = e.putOrderFk + LEFT JOIN vn.item i2 ON i2.supplyResponseFk = po.supplyResponseID + LEFT JOIN vn.ektEntryAssign eea ON eea.sub = e.sub + LEFT JOIN edi.item_groupToOffer igto ON igto.group_code = i.group_id + WHERE e.id = vSelf + LIMIT 1; + + IF NOT vHasItemGroup THEN + + CALL vn.mail_insert('logistica@verdnatura.es', 'nocontestar@verdnatura.es', 'Nuevo grupo en Floramondo', vDescription); + + CALL vn.mail_insert('pako@verdnatura.es', 'nocontestar@verdnatura.es', CONCAT('Nuevo grupo en Floramondo: ', vDescription), vDescription); + + LEAVE proc; + + END IF; + + -- Asigna la entrada + SELECT vn.ekt_getEntry(vSelf) INTO vEntryFk; + + -- Inserta el cubo si no existe + + IF vPackage = 800 THEN + + SET vHasToChangePackagingFk = TRUE; + + IF vItem THEN + + SELECT vn.item_getPackage(vItem) INTO vPackage ; + + ELSE + + SET vPackage = 8000 + vQty; + + INSERT IGNORE INTO vn.packaging(id, width, `depth`) + SELECT vPackage, vc.ccLength / vQty, vc.ccWidth + FROM vn.volumeConfig vc; + + END IF; + + ELSE + + INSERT IGNORE INTO vn2008.Cubos (Id_Cubo, X, Y, Z) + SELECT bucket_id, ROUND(x_size/10), ROUND(y_size/10), ROUND(z_size/10) + FROM bucket WHERE bucket_id = vPackage; + + IF ROW_COUNT() > 0 + THEN + INSERT INTO vn2008.mail SET + `subject` = 'Cubo añadido', + `text` = CONCAT('Se ha añadido el cubo: ', vPackage), + `to` = 'ekt@verdnatura.es'; + END IF; + END IF; + + -- Si es una compra de Logiflora obtiene el articulo + IF vPutOrderFk THEN + + SELECT i.id INTO vItem + FROM edi.putOrder po + JOIN vn.item i ON i.supplyResponseFk = po.supplyResponseID + WHERE po.id = vPutOrderFk + LIMIT 1; + + END IF; + + INSERT IGNORE INTO item_track SET + item_id = vRef; + + IF IFNULL(vItem,0) = 0 THEN + + -- Intenta obtener el artículo en base a los atributos holandeses + + SELECT b.id, IFNULL(b.itemOriginalFk ,b.itemFk) INTO vBuy, vItem + FROM edi.ekt e + JOIN edi.item_track t ON t.item_id = e.ref + LEFT JOIN edi.ekt l ON l.ref = e.ref + LEFT JOIN vn.buy b ON b.ektFk = l.id + LEFT JOIN vn.item i ON i.id = b.itemFk + JOIN vn2008.config cfg + WHERE e.id = vSelf + AND l.id != vSelf + AND b.itemFk != cfg.generic_item + AND IF(t.s1, l.s1 = e.s1, TRUE) + AND IF(t.s2, l.s2 = e.s2, TRUE) + AND IF(t.s3, l.s3 = e.s3, TRUE) + AND IF(t.s4, l.s4 = e.s4, TRUE) + AND IF(t.s5, l.s5 = e.s5, TRUE) + AND IF(t.s6, l.s6 = e.s6, TRUE) + AND IF(t.pac, l.pac = e.pac, TRUE) + AND IF(t.cat, l.cat = e.cat, TRUE) + AND IF(t.ori, l.ori = e.ori, TRUE) + AND IF(t.pro, l.pro = e.pro, TRUE) + AND IF(t.package, l.package = e.package, TRUE) + AND IF(t.item, l.item = e.item, TRUE) + AND i.isFloramondo = vIsFloramondoDirect + ORDER BY l.now DESC, b.id ASC + LIMIT 1; + + END IF; + + -- Si no encuentra el articulo lo crea en el caso de las compras directas en Floramondo + IF ISNULL(vItem) AND vIsFloramondoDirect THEN + + CALL edi.item_getNewByEkt(vSelf, vItem); + + END IF; + + INSERT INTO vn.buy + ( + entryFk + ,ektFk + ,buyingValue + ,itemFk + ,stickers + ,packing + ,`grouping` + ,quantity + ,groupingMode + ,packagingFk + ,weight + ) + SELECT + vEntryFk + ,vSelf + ,(@t := IF(i.stems, i.stems, 1)) * e.pri / IFNULL(i.stemMultiplier, 1) buyingValue + ,IFNULL(vItem, cfg.generic_item) itemFk + ,e.qty stickers + ,@pac := IFNULL(i.stemMultiplier, 1) * e.pac / @t packing + ,IFNULL(b.`grouping`, e.pac) + ,@pac * e.qty + ,vForceToPacking + ,IF(vHasToChangePackagingFk OR ISNULL(b.packagingFk), vPackage, b.packagingFk) + ,(IFNULL(i.weightByPiece,0) * @pac)/1000 + FROM edi.ekt e + LEFT JOIN vn.buy b ON b.id = vBuy + LEFT JOIN vn.item i ON i.id = b.itemFk + LEFT JOIN vn.supplier s ON e.pro = s.id + JOIN vn2008.config cfg + WHERE e.id = vSelf + LIMIT 1; + + DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc; + + CREATE TEMPORARY TABLE tmp.buyRecalc + SELECT buy.id + FROM vn.buy + WHERE ektFk = vSelf; + + CALL vn.buy_recalcPrices(); + + -- Si es una compra de Logiflora hay que informar la tabla vn.saleBuy + IF vPutOrderFk THEN + + REPLACE vn.saleBuy(saleFk, buyFk, workerFk) + SELECT po.saleFk, b.id, account.myUser_getId() + FROM edi.putOrder po + JOIN vn.buy b ON b.ektFk = vSelf + WHERE po.id = vPutOrderFk; + + END IF; + -- Si es una compra directa en Floramondo hay que añadirlo al ticket + + IF vIsFloramondoDirect THEN + + SELECT t.id INTO vTicketFk + FROM vn.ticket t + JOIN vn.ektEntryAssign eea + ON eea.addressFk = t.addressFk + AND t.warehouseFk = eea.warehouseInFk + JOIN edi.ekt e + ON e.sub = eea.sub + AND e.id = vSelf + WHERE e.fec = t.shipped + LIMIT 1; + + IF ISNULL(vTicketFk) THEN + + INSERT INTO vn.ticket ( + clientFk, + shipped, + addressFk, + agencyModeFk, + nickname, + warehouseFk, + companyFk, + landed, + zoneFk, + zonePrice, + zoneBonus + ) + SELECT + a.clientFk, + e.fec, + a.id, + a.agencyModeFk, + a.nickname, + eea.warehouseInFk, + c.id, + e.fec, + z.id, + z.price, + z.bonus + FROM edi.ekt e + JOIN vn.ektEntryAssign eea ON eea.sub = e.sub + JOIN vn.address a ON a.id = eea.addressFk + JOIN vn.company c ON c.code = 'VNL' + JOIN vn.`zone` z ON z.code = 'FLORAMONDO' + WHERE e.id = vSelf + LIMIT 1; + + SET vTicketFk = LAST_INSERT_ID(); + + INSERT INTO vn.ticketLog + SET originFk = vTicketFk, + userFk = account.myUser_getId(), + `action` = 'insert', + description = CONCAT('EktLoad ha creado el ticket:', ' ', vTicketFk); + + END IF; + + INSERT INTO vn.sale (itemFk, ticketFk, concept, quantity, price) + SELECT vItem, vTicketFk, e.item, e.qty * e.pac, e.pri * ( 1 + fhc.floramondoMargin ) + FROM edi.ekt e + JOIN edi.floraHollandConfig fhc + WHERE e.id = vSelf; + + SELECT LAST_INSERT_ID() INTO vSaleFk; + + REPLACE vn.saleBuy(saleFk, buyFk, workerFk) + SELECT vSaleFk, b.id, account.myUser_getId() + FROM vn.buy b + WHERE b.ektFk = vSelf; + + INSERT INTO vn.saleComponent(saleFk, componentFk, value) + SELECT vSaleFk, c.id, e.pri + FROM edi.ekt e + JOIN vn.component c ON c.code = 'purchaseValue' + WHERE e.id = vSelf; + + INSERT INTO vn.saleComponent(saleFk, componentFk, value) + SELECT vSaleFk, c.id, e.pri * fhc.floramondoMargin + FROM edi.ekt e + JOIN edi.floraHollandConfig fhc + JOIN vn.component c ON c.code = 'margin' + WHERE e.id = vSelf; + END IF; + DROP TEMPORARY TABLE tmp.buyRecalc; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `hedera`.`item_getVisible`( + vWarehouse TINYINT, + vDate DATE, + vType INT, + vPrefix VARCHAR(255)) +BEGIN + +/** + * Gets visible items of the specified type at specified date. + * + * @param vWarehouse The warehouse id + * @param vDate The visible date + * @param vType The type id + * @param vPrefix The article prefix to filter or %NULL for all + * @return tmp.itemVisible Visible items + */ + DECLARE vPrefixLen SMALLINT; + DECLARE vFilter VARCHAR(255) DEFAULT NULL; + DECLARE vDateInv DATE DEFAULT vn2008.date_inv(); + DECLARE EXIT HANDLER FOR 1114 + BEGIN + GET DIAGNOSTICS CONDITION 1 + @message = MESSAGE_TEXT; + CALL vn.mail_insert( + 'cau@verdnatura.es', + NULL, + CONCAT('hedera.item_getVisible error: ', @message), + CONCAT( + 'warehouse: ', IFNULL(vWarehouse, ''), + ', Fecha:', IFNULL(vDate, ''), + ', tipo: ', IFNULL(vType,''), + ', prefijo: ', IFNULL(vPrefix,''))); + RESIGNAL; + END; + SET vPrefixLen = IFNULL(LENGTH(vPrefix), 0) + 1; + + IF vPrefixLen > 1 THEN + SET vFilter = CONCAT(vPrefix, '%'); + END IF; + + DROP TEMPORARY TABLE IF EXISTS `filter`; + CREATE TEMPORARY TABLE `filter` + (INDEX (itemFk)) + ENGINE = MEMORY + SELECT id itemFk FROM vn.item + WHERE typeFk = vType + AND (vFilter IS NULL OR `name` LIKE vFilter); + + DROP TEMPORARY TABLE IF EXISTS currentStock; + CREATE TEMPORARY TABLE currentStock + (INDEX (itemFk)) + ENGINE = MEMORY + SELECT itemFk, SUM(quantity) quantity + FROM ( + SELECT b.itemFk, b.quantity + FROM vn.buy b + JOIN vn.entry e ON e.id = b.entryFk + JOIN vn.travel t ON t.id = e.travelFk + WHERE t.landed BETWEEN vDateInv AND vDate + AND t.warehouseInFk = vWarehouse + AND NOT e.isRaid + UNION ALL + SELECT b.itemFk, -b.quantity + FROM vn.buy b + JOIN vn.entry e ON e.id = b.entryFk + JOIN vn.travel t ON t.id = e.travelFk + WHERE t.shipped BETWEEN vDateInv AND util.VN_CURDATE() + AND t.warehouseOutFk = vWarehouse + AND NOT e.isRaid + AND t.isDelivered + UNION ALL + SELECT m.itemFk, -m.quantity + FROM vn.sale m + JOIN vn.ticket t ON t.id = m.ticketFk + JOIN vn.ticketState s ON s.ticket = t.id + WHERE t.shipped BETWEEN vDateInv AND util.VN_CURDATE() + AND t.warehouseFk = vWarehouse + AND s.alertLevel = 3 + ) t + GROUP BY itemFk + HAVING quantity > 0; + + DROP TEMPORARY TABLE IF EXISTS tmp; + CREATE TEMPORARY TABLE tmp + (INDEX (itemFk)) + ENGINE = MEMORY + SELECT * + FROM ( + SELECT b.itemFk, b.packagingFk, b.packing + FROM vn.buy b + JOIN vn.entry e ON e.id = b.entryFk + JOIN vn.travel t ON t.id = e.travelFk + WHERE t.landed BETWEEN vDateInv AND vDate + AND NOT b.isIgnored + AND b.price2 >= 0 + AND b.packagingFk IS NOT NULL + ORDER BY t.warehouseInFk = vWarehouse DESC, t.landed DESC + LIMIT 10000000000000000000 + ) t GROUP BY itemFk; + + DROP TEMPORARY TABLE IF EXISTS tmp.itemVisible; + CREATE TEMPORARY TABLE tmp.itemVisible + ENGINE = MEMORY + SELECT i.id Id_Article, + SUBSTRING(i.`name`, vPrefixLen) Article, + t.packing, p.id Id_Cubo, + IF(p.depth > 0, p.depth, 0) depth, p.width, p.height, + CEIL(s.quantity / t.packing) etiquetas + FROM vn.item i + JOIN `filter` f ON f.itemFk = i.id + JOIN currentStock s ON s.itemFk = i.id + LEFT JOIN tmp t ON t.itemFk = i.id + LEFT JOIN vn.packaging p ON p.id = t.packagingFk + WHERE CEIL(s.quantity / t.packing) > 0 + -- FIXME: Column Cubos.box not included in view vn.packaging + /* AND p.box */; + + DROP TEMPORARY TABLE + `filter`, + currentStock, + tmp; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`buy_getVolume`() +BEGIN +/** + * Cálculo de volumen en líneas de compra + * @table tmp.buy(buyFk) + */ + SELECT t.name Temp, + CAST(ROUND(SUM(GREATEST(b.stickers ,b.quantity /b.packing ) * + item_getVolume(b.itemFk, b.packagingFk)) / vc.trolleyM3 / 1000000 ,1) AS DECIMAL(10,2)) carros , + CAST(ROUND(SUM(GREATEST(b.stickers ,b.quantity /b.packing ) * + item_getVolume(b.itemFk, b.packagingFk)) / vc.palletM3 / 1000000,1) AS DECIMAL(10,2)) espais + FROM buy b + JOIN tmp.buy tb ON tb.buyFk = b.id + JOIN volumeConfig vc + JOIN item i ON i.id = b.itemFk + JOIN itemType it ON it.id = i.typeFk + LEFT JOIN temperature t ON t.code = it.temperatureFk + GROUP BY Temp; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`entry_checkPackaging`(vEntryFk INT) +BEGIN +/** + * Comprueba que los campos package y packaging no sean nulos + * + * @param vEntryFk Id de entrada + */ + DECLARE vpackageOrPackingNull INT; + + SELECT count(*) INTO vpackageOrPackingNull + FROM buy b + WHERE b.entryFk = vEntryFk + AND (b.packing IS NULL OR b.packagingFk IS NULL); + + IF vpackageOrPackingNull THEN + CALL util.throw("packageOrPackingNull"); + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`fustControl`(vFromDated DATE, vToDated DATE) +BEGIN + + DECLARE vSijsnerClientFk INT DEFAULT 19752; + + DECLARE vDateStart DATETIME; + DECLARE vDateEnd DATETIME; + + SET vDateStart = vFromDated; + SET vDateEnd = util.Dayend(vToDated); + + SELECT p.id FustCode, + CAST(sent.stucks AS DECIMAL(10,0)) FH, + CAST(tp.stucks AS DECIMAL(10,0)) Tickets, + CAST(-sj.stucks AS DECIMAL(10,0)) Sijsner, + CAST(IFNULL(sent.stucks,0) - IFNULL(tp.stucks,0) + IFNULL(sj.stucks,0) AS DECIMAL(10,0)) saldo + FROM vn.packaging p + LEFT JOIN ( + SELECT FustCode, sum(fustQuantity) stucks + FROM ( + SELECT IFNULL(pe.equivalentFk ,b.packagingFk) FustCode, s.quantity / b.packing AS fustQuantity + FROM vn.sale s + JOIN vn.ticket t ON t.id = s.ticketFk + JOIN vn.warehouse w ON w.id = t.warehouseFk + JOIN vn.warehouseAlias wa ON wa.id = w.aliasFk + JOIN cache.last_buy lb ON lb.item_id = s.itemFk AND lb.warehouse_id = t.warehouseFk + JOIN vn.buy b ON b.id = lb.buy_id + JOIN vn.packaging p ON p.id = b.packagingFk + LEFT JOIN vn.packageEquivalent pe ON pe.packagingFk = p.id + JOIN vn.address a ON a.id = t.addressFk + JOIN vn.province p2 ON p2.id = a.provinceFk + JOIN vn.country c ON c.id = p2.countryFk + WHERE t.shipped BETWEEN vDateStart AND vDateEnd + AND wa.name = 'VNH' + AND p.isPackageReturnable + AND c.country = 'FRANCIA') sub + GROUP BY FustCode) sent ON sent.FustCode = p.id + LEFT JOIN ( + SELECT FustCode, sum(quantity) stucks + FROM ( + SELECT IFNULL(pe.equivalentFk ,tp.packagingFk) FustCode, tp.quantity + FROM vn.ticketPackaging tp + JOIN vn.ticket t ON t.id = tp.ticketFk + JOIN vn.warehouse w ON w.id = t.warehouseFk + JOIN vn.warehouseAlias wa ON wa.id = w.aliasFk + JOIN vn.packaging p ON p.id = tp.packagingFk + LEFT JOIN vn.packageEquivalent pe ON pe.packagingFk = p.id + JOIN vn.address a ON a.id = t.addressFk + JOIN vn.province p2 ON p2.id = a.provinceFk + JOIN vn.country c ON c.id = p2.countryFk + WHERE t.shipped BETWEEN vDateStart AND vDateEnd + AND wa.name = 'VNH' + AND p.isPackageReturnable + AND c.country = 'FRANCIA' + AND t.clientFk != vSijsnerClientFk + AND tp.quantity > 0) sub + GROUP BY FustCode) tp ON tp.FustCode = p.id + LEFT JOIN ( + SELECT FustCode, sum(quantity) stucks + FROM ( + SELECT IFNULL(pe.equivalentFk ,tp.packagingFk) FustCode, tp.quantity + FROM vn.ticketPackaging tp + JOIN vn.ticket t ON t.id = tp.ticketFk + JOIN vn.warehouse w ON w.id = t.warehouseFk + JOIN vn.warehouseAlias wa ON wa.id = w.aliasFk + JOIN vn.packaging p ON p.id = tp.packagingFk + LEFT JOIN vn.packageEquivalent pe ON pe.packagingFk = p.id + WHERE t.shipped BETWEEN TIMESTAMPADD(DAY, 1, vDateStart ) AND TIMESTAMPADD(DAY, 1, vDateEnd ) + AND wa.name = 'VNH' + AND p.isPackageReturnable + AND t.clientFk = vSijsnerClientFk) sub + GROUP BY FustCode) sj ON sj.FustCode = p.id + WHERE sent.stucks + OR tp.stucks + OR sj.stucks; + +END$$ +DELIMITER ; + diff --git a/db/changes/234601/.gitkeep b/db/changes/234601/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/db/changes/234601/00-ACLticketTrackingState.sql b/db/changes/234601/00-ACLticketTrackingState.sql new file mode 100644 index 000000000..ca6dce0c9 --- /dev/null +++ b/db/changes/234601/00-ACLticketTrackingState.sql @@ -0,0 +1,4 @@ +UPDATE `salix`.`ACL` + SET `property` = 'state', + `model` = 'Ticket' + WHERE `property` = 'changeState'; diff --git a/db/changes/234601/00-addressShortage.sql b/db/changes/234601/00-addressShortage.sql new file mode 100644 index 000000000..57c07d480 --- /dev/null +++ b/db/changes/234601/00-addressShortage.sql @@ -0,0 +1,98 @@ + +-- Place your SQL code here + +ALTER TABLE `vn`.`productionConfig` ADD shortageAddressFk int(11) COMMENT 'Consignatario por defecto para añadir un item de alta'; +ALTER TABLE `vn`.`productionConfig` ADD CONSTRAINT productionConfig_FK FOREIGN KEY (shortageAddressFk) REFERENCES vn.address(id) ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE `vn`.`sale` MODIFY COLUMN originalQuantity double(9,1) DEFAULT NULL NULL COMMENT 'Se utiliza para notificar a través de rocket los cambios de quantity'; + +INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId) VALUES( 'AddressShortage', '*', 'READ', 'ALLOW', 'ROLE', 'production'); + +-- vn.addressShortage definition + +CREATE TABLE `vn`.`addressShortage` ( + `addressFk` int(11) NOT NULL, + PRIMARY KEY (`addressFk`), + CONSTRAINT `addressShortage_FK` FOREIGN KEY (`addressFk`) REFERENCES `address` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + + +DELIMITER $$ + +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`item_setVisibleDiscard`( + vItemFk INT, + vWarehouseFk INT, + vQuantity INT, + vAddressFk INT) +BEGIN +/** + * Procedimiento para dar dar de baja/alta un item, si vAddressFk es NULL se entiende que se da de alta y se toma el addressFk de la configuración + * + * @param vItemFk Identificador del ítem + * @param vWarehouseFk id del warehouse + * @param vQuantity a dar de alta/baja + * @param vAddressFk id address + */ + DECLARE vTicketFk INT; + DECLARE vClientFk INT; + DECLARE vDefaultCompanyFk INT; + DECLARE vCalc INT; + DECLARE vAddressShortage INT; + + SELECT barcodeToItem(vItemFk) INTO vItemFk; + + SELECT DEFAULT(companyFk) INTO vDefaultCompanyFk + FROM vn.ticket LIMIT 1; + + IF vAddressFk IS NULL THEN + SELECT pc.shortageAddressFk INTO vAddressShortage + FROM productionConfig pc ; + ELSE + SET vAddressShortage = vAddressFk; + END IF; + + SELECT a.clientFk INTO vClientFk + FROM address a + WHERE a.id = vAddressFk; + + SELECT t.id INTO vTicketFk + FROM ticket t + JOIN address a ON a.id = t.addressFk + JOIN ticketState ts ON ts.ticketFk = t.id + WHERE t.warehouseFk = vWarehouseFk + AND a.id = vAddressShortage + AND DATE(t.shipped) = util.VN_CURDATE() + AND ts.code = 'DELIVERED' + LIMIT 1; + + CALL cache.visible_refresh(vCalc, TRUE, vWarehouseFk); + + IF vTicketFk IS NULL THEN + CALL ticket_add( + vClientFk, + util.VN_CURDATE(), + vWarehouseFk, + vDefaultCompanyFk, + vAddressFk, + NULL, + NULL, + util.VN_CURDATE(), + account.myUser_getId(), + FALSE, + vTicketFk); + END IF; + + INSERT INTO sale(ticketFk, itemFk, concept, quantity) + SELECT vTicketFk, + vItemFk, + CONCAT(longName,' ', worker_getCode(), ' ', LEFT(CAST(util.VN_NOW() AS TIME),5)), + vQuantity + FROM item + WHERE id = vItemFk; + + UPDATE cache.visible + SET visible = visible - vQuantity + WHERE calc_id = vCalc + AND item_id = vItemFk; +END$$ +DELIMITER ; diff --git a/db/changes/233601/00-createClaimReader.sql b/db/changes/234601/00-claimViewerAcl.sql similarity index 86% rename from db/changes/233601/00-createClaimReader.sql rename to db/changes/234601/00-claimViewerAcl.sql index e913c0ed9..17d8d4ce0 100644 --- a/db/changes/233601/00-createClaimReader.sql +++ b/db/changes/234601/00-claimViewerAcl.sql @@ -21,11 +21,11 @@ DELETE FROM `salix`.`ACL` 'getSummary' ); -INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalid`) +INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`) VALUES ('Claim','filter','READ','ALLOW','ROLE','claimViewer'); -INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalid`) +INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`) VALUES ('Claim','find','READ','ALLOW','ROLE','claimViewer'); -INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalid`) +INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`) VALUES ('Claim','findById','READ','ALLOW','ROLE','claimViewer'); -INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalid`) - VALUES ('Claim','getSummary','READ','ALLOW','ROLE','claimViewer'); \ No newline at end of file +INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`) + VALUES ('Claim','getSummary','READ','ALLOW','ROLE','claimViewer'); diff --git a/db/changes/234601/00-clientAfterUpdate.sql b/db/changes/234601/00-clientAfterUpdate.sql new file mode 100644 index 000000000..c6483813f --- /dev/null +++ b/db/changes/234601/00-clientAfterUpdate.sql @@ -0,0 +1,95 @@ +ALTER TABLE `vn`.`client` MODIFY COLUMN `credit` decimal(10,2) unsigned DEFAULT 0.00 NOT NULL; + +DELETE FROM `salix`.`ACL` WHERE `model` = 'Client' AND `property` = 'create'; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`client_beforeUpdate` + BEFORE UPDATE ON `client` + FOR EACH ROW +BEGIN + DECLARE vText VARCHAR(255) DEFAULT NULL; + DECLARE vPayMethodFk INT; + + SET NEW.editorFk = account.myUser_getId(); + + IF NOT(NEW.credit <=> OLD.credit) THEN + INSERT INTO clientCredit + SET clientFk = NEW.id, + amount = NEW.credit, + workerFk = NEW.editorFk; + END IF; + -- Comprueba que el formato de los teléfonos es válido + + IF !(NEW.phone <=> OLD.phone) AND (NEW.phone <> '') THEN + CALL pbx.phone_isValid(NEW.phone); + END IF; + + IF !(NEW.mobile <=> OLD.mobile) AND (NEW.mobile <> '')THEN + CALL pbx.phone_isValid(NEW.mobile); + END IF; + + SELECT id INTO vPayMethodFk + FROM vn.payMethod + WHERE code = 'bankDraft'; + + IF NEW.payMethodFk = vPayMethodFk AND NEW.dueDay = 0 THEN + SET NEW.dueDay = 5; + END IF; + + -- Avisar al comercial si ha llegado la documentación sepa/core + + IF NEW.hasSepaVnl AND !OLD.hasSepaVnl THEN + SET vText = 'Sepa de VNL'; + END IF; + + IF NEW.hasCoreVnl AND !OLD.hasCoreVnl THEN + SET vText = 'Core de VNL'; + END IF; + + IF vText IS NOT NULL + THEN + INSERT INTO mail(receiver, replyTo, `subject`, body) + SELECT + CONCAT(IF(ac.id,u.name, 'jgallego'), '@verdnatura.es'), + 'administracion@verdnatura.es', + CONCAT('Cliente ', NEW.id), + CONCAT('Recibida la documentación: ', vText) + FROM worker w + LEFT JOIN account.user u ON w.id = u.id AND u.active + LEFT JOIN account.account ac ON ac.id = u.id + WHERE w.id = NEW.salesPersonFk; + END IF; + + IF NEW.salespersonFk IS NULL AND OLD.salespersonFk IS NOT NULL THEN + IF (SELECT COUNT(clientFk) + FROM clientProtected + WHERE clientFk = NEW.id + ) > 0 THEN + CALL util.throw("HAS_CLIENT_PROTECTED"); + END IF; + END IF; + + IF !(NEW.salesPersonFk <=> OLD.salesPersonFk) THEN + SET NEW.lastSalesPersonFk = IFNULL(NEW.salesPersonFk, OLD.salesPersonFk); + END IF; + + IF !(NEW.businessTypeFk <=> OLD.businessTypeFk) AND (NEW.businessTypeFk = 'individual' OR OLD.businessTypeFk = 'individual') THEN + SET NEW.isTaxDataChecked = 0; + END IF; +END$$ +DELIMITER ; + +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`client_AfterInsert` + AFTER INSERT ON `client` + FOR EACH ROW +BEGIN + IF NEW.credit IS NOT NULL AND NEW.credit THEN + INSERT INTO clientCredit + SET clientFk = NEW.id, + workerFk = NEW.editorFk, + amount = NEW.credit; + END IF; +END$$ +DELIMITER ; + diff --git a/db/changes/234601/00-expeditionStateBeforeInsert.sql b/db/changes/234601/00-expeditionStateBeforeInsert.sql new file mode 100644 index 000000000..565b3ded3 --- /dev/null +++ b/db/changes/234601/00-expeditionStateBeforeInsert.sql @@ -0,0 +1,13 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`expeditionState_BeforeInsert` + BEFORE INSERT ON `expeditionState` + FOR EACH ROW +BEGIN + + SET NEW.userFk = IFNULL(NEW.userFk, account.myUser_getId()); + +END$$ +DELIMITER ; + + + diff --git a/db/changes/234601/00-ticketSetStateRefactor.sql b/db/changes/234601/00-ticketSetStateRefactor.sql new file mode 100644 index 000000000..9078ffb0e --- /dev/null +++ b/db/changes/234601/00-ticketSetStateRefactor.sql @@ -0,0 +1,55 @@ +DELIMITER $$ +CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_setState`( + vSelf INT, + vStateCode VARCHAR(255) COLLATE utf8_general_ci +) +BEGIN +/** + * Modifica el estado de un ticket si se cumplen las condiciones necesarias. + * + * @param vSelf el id del ticket + * @param vStateCode estado a modificar del ticket + */ + DECLARE vticketAlertLevel INT; + DECLARE vTicketStateCode VARCHAR(255); + DECLARE vCanChangeState BOOL; + DECLARE vPackedAlertLevel INT; + DECLARE vZoneFk INT; + + SELECT s.alertLevel, s.`code`, t.zoneFk + INTO vticketAlertLevel, vTicketStateCode, vZoneFk + FROM state s + JOIN ticketTracking tt ON tt.stateFk = s.id + JOIN ticket t ON t.id = tt.ticketFk + WHERE tt.ticketFk = vSelf + ORDER BY tt.created DESC + LIMIT 1; + + SELECT id INTO vPackedAlertLevel FROM alertLevel WHERE code = 'PACKED'; + + IF vStateCode = 'OK' AND vZoneFk IS NULL THEN + CALL util.throw('ASSIGN_ZONE_FIRST'); + END IF; + + SET vCanChangeState = ( + vStateCode <> 'ON_CHECKING' OR + vticketAlertLevel < vPackedAlertLevel + )AND NOT ( + vTicketStateCode IN ('CHECKED', 'CHECKING') + AND vStateCode IN ('PREPARED', 'ON_PREPARATION') + ); + + IF vCanChangeState THEN + INSERT INTO ticketTracking (stateFk, ticketFk, workerFk) + SELECT id, vSelf, account.myUser_getId() + FROM state + WHERE `code` = vStateCode COLLATE utf8_unicode_ci; + + IF vStateCode = 'PACKED' THEN + CALL ticket_doCmr(vSelf); + END IF; + ELSE + CALL util.throw('INCORRECT_TICKET_STATE'); + END IF; +END$$ +DELIMITER ; diff --git a/db/changes/234601/00-transferInvoice.sql b/db/changes/234601/00-transferInvoice.sql new file mode 100644 index 000000000..7a9890ae4 --- /dev/null +++ b/db/changes/234601/00-transferInvoice.sql @@ -0,0 +1,6 @@ +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES + ('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('CplusInvoiceType477', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); diff --git a/db/changes/234801/.gitkeep b/db/changes/234801/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/db/changes/234801/00-notificationSubscription.sql b/db/changes/234801/00-notificationSubscription.sql new file mode 100644 index 000000000..ef081437d --- /dev/null +++ b/db/changes/234801/00-notificationSubscription.sql @@ -0,0 +1,3 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('NotificationSubscription', 'getList', 'READ', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index b6c89bd40..499336243 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -470,22 +470,22 @@ CREATE TEMPORARY TABLE tmp.address WHERE `defaultAddressFk` IS NULL; DROP TEMPORARY TABLE tmp.address; -INSERT INTO `vn`.`clientCredit`(`id`, `clientFk`, `workerFk`, `amount`, `created`) +INSERT INTO `vn`.`clientCredit`(`clientFk`, `workerFk`, `amount`, `created`) VALUES - (1 , 1101, 5, 300, DATE_ADD(util.VN_CURDATE(), INTERVAL -11 MONTH)), - (2 , 1101, 5, 900, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 MONTH)), - (3 , 1101, 5, 800, DATE_ADD(util.VN_CURDATE(), INTERVAL -9 MONTH)), - (4 , 1101, 5, 700, DATE_ADD(util.VN_CURDATE(), INTERVAL -8 MONTH)), - (5 , 1101, 5, 600, DATE_ADD(util.VN_CURDATE(), INTERVAL -7 MONTH)), - (6 , 1101, 5, 500, DATE_ADD(util.VN_CURDATE(), INTERVAL -6 MONTH)), - (7 , 1101, 5, 400, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 MONTH)), - (8 , 1101, 9, 300, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)), - (9 , 1101, 9, 200, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)), - (10, 1101, 9, 100, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)), - (11, 1101, 9, 50 , DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)), - (12, 1102, 9, 800, util.VN_CURDATE()), - (14, 1104, 9, 90 , util.VN_CURDATE()), - (15, 1105, 9, 90 , util.VN_CURDATE()); + (1101, 5, 300, DATE_ADD(util.VN_CURDATE(), INTERVAL -11 MONTH)), + (1101, 5, 900, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 MONTH)), + (1101, 5, 800, DATE_ADD(util.VN_CURDATE(), INTERVAL -9 MONTH)), + (1101, 5, 700, DATE_ADD(util.VN_CURDATE(), INTERVAL -8 MONTH)), + (1101, 5, 600, DATE_ADD(util.VN_CURDATE(), INTERVAL -7 MONTH)), + (1101, 5, 500, DATE_ADD(util.VN_CURDATE(), INTERVAL -6 MONTH)), + (1101, 5, 400, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 MONTH)), + (1101, 9, 300, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH)), + (1101, 9, 200, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH)), + (1101, 9, 100, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)), + (1101, 9, 50 , DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)), + (1102, 9, 800, util.VN_CURDATE()), + (1104, 9, 90 , util.VN_CURDATE()), + (1105, 9, 90 , util.VN_CURDATE()); INSERT INTO `vn`.`clientCreditLimit`(`id`, `maxAmount`, `roleFk`) VALUES @@ -604,7 +604,7 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`) VALUES - (1, 'T', 1014.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), + (1, 'T', 1026.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (2, 'T', 121.36, util.VN_CURDATE(), 1102, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (3, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (4, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), @@ -1454,7 +1454,7 @@ INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `itemFk`, `itemTypeF ('HankPym', YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', 6, 1, '186', '0', '0.0'), ('HankPym', YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', 7, 1, '277', '0', '0.0'); -INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`, `printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`) +INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packagingFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`, `printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`) VALUES (1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)), (2, 2, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)), @@ -2003,6 +2003,10 @@ UPDATE `vn`.`business` b SET b.`departmentFk` = 43 WHERE b.id IN(18, 19); +UPDATE `vn`.`business` b + SET b.`started` = b.`started` - INTERVAL 100 DAY + WHERE b.id = 1107; + INSERT INTO `vn`.`workCenterHoliday` (`workCenterFk`, `days`, `year`) VALUES ('1', '27.5', YEAR(util.VN_CURDATE())), @@ -2051,22 +2055,22 @@ INSERT INTO `vn`.`absenceType` (`id`, `name`, `rgb`, `code`, `holidayEntitlement INSERT INTO `vn`.`calendar` (`businessFk`, `dayOffTypeFk`, `dated`) VALUES - (1, 6, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 10 DAY))), - (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 10 DAY))), - (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -11 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 11 DAY))), - (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -12 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 12 DAY))), - (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -20 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 20 DAY))), - (1106, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -13 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 8 DAY))), - (1106, 1, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -14 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 9 DAY))), - (1106, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 7 DAY))), - (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 10 DAY))), - (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -11 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 11 DAY))), - (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -12 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 12 DAY))), - (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, DATE_ADD(util.VN_CURDATE(), INTERVAL -20 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 20 DAY))), - (1107, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -13 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 8 DAY))), - (1107, 1, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -14 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 9 DAY))), - (1107, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), DATE_ADD(util.VN_CURDATE(), INTERVAL 7 DAY))), - (1107, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL - 16 DAY)); + (1, 6, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 10 DAY, util.VN_CURDATE() + INTERVAL 10 DAY)), + (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 10 DAY, util.VN_CURDATE() + INTERVAL 10 DAY)), + (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 11 DAY, util.VN_CURDATE() + INTERVAL 11 DAY)), + (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 12 DAY, util.VN_CURDATE() + INTERVAL 12 DAY)), + (1106, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 20 DAY, util.VN_CURDATE() + INTERVAL 20 DAY)), + (1106, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 13 DAY, util.VN_CURDATE() + INTERVAL 8 DAY)), + (1106, 1, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 14 DAY, util.VN_CURDATE() + INTERVAL 9 DAY)), + (1106, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 15 DAY, util.VN_CURDATE() + INTERVAL 7 DAY)), + (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 10 DAY, util.VN_CURDATE() + INTERVAL 10 DAY)), + (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 11 DAY, util.VN_CURDATE() + INTERVAL 11 DAY)), + (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 12 DAY, util.VN_CURDATE() + INTERVAL 12 DAY)), + (1107, 1, IF(MONTH(util.VN_CURDATE()) = 12 AND DAY(util.VN_CURDATE()) > 10, util.VN_CURDATE() - INTERVAL 20 DAY, util.VN_CURDATE() + INTERVAL 20 DAY)), + (1107, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 13 DAY, util.VN_CURDATE() + INTERVAL 8 DAY)), + (1107, 1, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 14 DAY, util.VN_CURDATE() + INTERVAL 9 DAY)), + (1107, 2, IF(MONTH(util.VN_CURDATE()) >= 1 AND DAY(util.VN_CURDATE()) > 20, util.VN_CURDATE() - INTERVAL 15 DAY, util.VN_CURDATE() + INTERVAL 7 DAY)), + (1107, 2, util.VN_CURDATE() - INTERVAL 16 DAY); INSERT INTO `vn`.`smsConfig` (`id`, `uri`, `title`, `apiKey`) VALUES @@ -2754,9 +2758,9 @@ INSERT INTO `vn`.`sectorCollectionSaleGroup` (`sectorCollectionFk`, `saleGroupFk VALUES (1, 1); -INSERT INTO `vn`.`workerTimeControlConfig` (`id`, `dayBreak`, `dayBreakDriver`, `shortWeekBreak`, `longWeekBreak`, `weekScope`, `mailPass`, `mailHost`, `mailSuccessFolder`, `mailErrorFolder`, `mailUser`, `minHoursToBreak`, `breakHours`, `hoursCompleteWeek`, `startNightlyHours`, `endNightlyHours`, `maxTimePerDay`, `breakTime`, `timeToBreakTime`, `dayMaxTime`, `shortWeekDays`, `longWeekDays`, `teleworkingStart`, `teleworkingStartBreakTime`) +INSERT INTO `vn`.`workerTimeControlConfig` (`id`, `dayBreak`, `dayBreakDriver`, `shortWeekBreak`, `longWeekBreak`, `weekScope`, `mailPass`, `mailHost`, `mailSuccessFolder`, `mailErrorFolder`, `mailUser`, `minHoursToBreak`, `breakHours`, `hoursCompleteWeek`, `startNightlyHours`, `endNightlyHours`, `maxTimePerDay`, `breakTime`, `timeToBreakTime`, `dayMaxTime`, `shortWeekDays`, `longWeekDays`, `teleworkingStart`, `teleworkingStartBreakTime`, `maxTimeToBreak`, `maxWorkShortCycle`, `maxWorkLongCycle`) VALUES - (1, 43200, 32400, 129600, 259200, 604800, '', '', 'Leidos.exito', 'Leidos.error', 'timeControl', 5.33, 0.33, 40, '22:00:00', '06:00:00', 57600, 1200, 18000, 57600, 6, 13, 28800, 32400); + (1, 43200, 32400, 129600, 259200, 1080000, '', 'imap.verdnatura.es', 'Leidos.exito', 'Leidos.error', 'timeControl', 5.00, 0.33, 40, '22:00:00', '06:00:00', 72000, 1200, 18000, 72000, 6, 13, 28800, 32400, 3600, 561600, 950400); INSERT INTO `vn`.`host` (`id`, `code`, `description`, `warehouseFk`, `bankFk`) VALUES @@ -2784,6 +2788,11 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`) INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) VALUES (1, 9), + (1, 1), + (2, 1), + (3, 9), + (4, 1), + (5, 9), (6, 9); INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`) @@ -2796,6 +2805,8 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`) VALUES (1, 1109), (1, 1110), + (2, 1110), + (4, 1110), (2, 1109), (1, 9), (1, 3), @@ -2867,6 +2878,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) VALUES ('lilium', 'development', 'http://localhost:9000/#/'), + ('hedera', 'development', 'http://localhost:9090/'), ('salix', 'development', 'http://localhost:5000/#!/'); INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) @@ -2976,3 +2988,9 @@ INSERT INTO vn.XDiario (id, ASIEN, FECHA, SUBCTA, CONTRA, CONCEPTO, EURODEBE, EU INSERT INTO `vn`.`mistakeType` (`id`, `description`) VALUES (1, 'Incorrect quantity'); + +INSERT INTO `vn`.`invoiceCorrectionType` (`id`, `description`) + VALUES + (1, 'Error in VAT calculation'), + (2, 'Error in sales details'), + (3, 'Error in customer data'); diff --git a/db/dump/structure.sql b/db/dump/structure.sql index 08df0541c..b242821fc 100644 --- a/db/dump/structure.sql +++ b/db/dump/structure.sql @@ -30434,6 +30434,7 @@ CREATE TABLE `item` ( `editorFk` int(10) unsigned DEFAULT NULL, `recycledPlastic` int(11) DEFAULT NULL, `nonRecycledPlastic` int(11) DEFAULT NULL, + `minQuantity` int(10) unsigned DEFAULT NULL COMMENT 'Cantidad mínima para una línea de venta', PRIMARY KEY (`id`), UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`), KEY `Color` (`inkFk`), diff --git a/db/tests/vn/ticketCalculateClon.spec.js b/db/tests/vn/ticketCalculateClon.spec.js index 9116d805f..665d52ed0 100644 --- a/db/tests/vn/ticketCalculateClon.spec.js +++ b/db/tests/vn/ticketCalculateClon.spec.js @@ -53,7 +53,8 @@ describe('ticket ticketCalculateClon()', () => { expect(result[orderIndex][0].ticketFk).toBeGreaterThan(newestTicketIdInFixtures); }); - it('should add the ticket to the order containing the original ticket and generate landed value if it was null', async() => { + it('should add the ticket to the order containing the original ' + + 'ticket and generate landed value if it was null', async() => { let stmts = []; let stmt; diff --git a/db/tests/vn/timeControl_calculateByUser.spec.js b/db/tests/vn/timeControl_calculateByUser.spec.js deleted file mode 100644 index 0b385d2c9..000000000 --- a/db/tests/vn/timeControl_calculateByUser.spec.js +++ /dev/null @@ -1,91 +0,0 @@ -const app = require('vn-loopback/server/server'); -const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; - -describe('timeControl_calculateByUser()', () => { - it(`should return today's worked hours`, async() => { - let start = Date.vnNew(); - start.setHours(0, 0, 0, 0); - start.setDate(start.getDate() - 1); - - let end = Date.vnNew(); - end.setHours(0, 0, 0, 0); - end.setDate(end.getDate() + 1); - - let stmts = []; - let stmt; - - let params = { - workerID: 1106, - start: start, - end: end - }; - - stmt = new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ - params.workerID, - params.start, - params.end - ]); - stmts.push(stmt); - - let tableIndex = stmts.push('SELECT * FROM tmp.timeControlCalculate') - 1; - - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await app.models.Ticket.rawStmt(sql); - - let [timeControlCalculateTable] = result[tableIndex]; - - expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28200); - }); - // #2261 - xit(`should return the worked hours between last sunday and monday`, async() => { - let lastSunday = Date.vnNew(); - let daysSinceSunday = lastSunday.getDay(); - if (daysSinceSunday === 0) // this means today is sunday but you need the previous sunday :) - daysSinceSunday = 7; - lastSunday.setHours(23, 0, 0, 0); - lastSunday.setDate(lastSunday.getDate() - daysSinceSunday); - - let monday = Date.vnNew(); - let daysSinceMonday = daysSinceSunday - 1; // aiming for monday (today could be monday) - monday.setHours(7, 0, 0, 0); - monday.setDate(monday.getDate() - daysSinceMonday); - - let stmts = []; - let stmt; - - stmts.push('START TRANSACTION'); - - const workerID = 1108; - - stmt = new ParameterizedSQL(` - INSERT INTO vn.workerTimeControl(userFk, timed, manual, direction) - VALUES - (?, ?, 1, 'in'), - (?, ?, 1, 'out') - `, [ - workerID, - lastSunday, - workerID, - monday - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ - workerID, - lastSunday, - monday - ]); - stmts.push(stmt); - - let tableIndex = stmts.push('SELECT * FROM tmp.timeControlCalculate') - 1; - - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await app.models.Ticket.rawStmt(sql); - - let [timeControlCalculateTable] = result[tableIndex]; - - expect(timeControlCalculateTable.timeWorkSeconds).toEqual(30000); - }); -}); diff --git a/db/tests/vn/workerTimeControlCheck.spec.js b/db/tests/vn/workerTimeControlCheck.spec.js deleted file mode 100644 index 0ca1429d4..000000000 --- a/db/tests/vn/workerTimeControlCheck.spec.js +++ /dev/null @@ -1,580 +0,0 @@ -const app = require('vn-loopback/server/server'); -const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; - -// #2261 xdescribe dbtest workerTimeControl_check() -xdescribe('worker workerTimeControl_check()', () => { - it(`should throw an error if the worker can't sign on that tablet`, async() => { - let stmts = []; - let stmt; - const workerId = 1110; - const tabletId = 2; - let err; - stmts.push('START TRANSACTION'); - try { - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - await app.models.Worker.rawStmt(sql); - } catch (e) { - err = e; - } - - expect(err.sqlMessage).toEqual('No perteneces a este departamento.'); - }); - - it('should check that the worker can sign on that tablet', async() => { - let stmts = []; - let stmt; - const workerId = 1110; - const tabletId = 1; - let err; - stmts.push('START TRANSACTION'); - try { - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - await app.models.Worker.rawStmt(sql); - } catch (e) { - err = e; - } - - expect(err).not.toBeDefined(); - }); - - it('should throw an error if the worker with a special category has not finished the 9h break', async() => { - const workerId = 1110; - const tabletId = 1; - let stmts = []; - let stmt; - let sql; - let error; - - stmts.push('START TRANSACTION'); - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-17,NOW()),0,"in"), - (?,TIMESTAMPADD(SECOND,-32399,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - error = e; - } - - expect(error.sqlMessage).toEqual('Descansos 9 h'); - }); - - it('should check f the worker with a special category has finished the 9h break', async() => { - const workerId = 1110; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - stmts.push('START TRANSACTION'); - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-17,NOW()),0,"in"), - (?,TIMESTAMPADD(SECOND,-32401,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err).not.toBeDefined(); - }); - - it('should throw an error if the worker has not finished the 12h break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let sql; - let error; - - stmts.push('START TRANSACTION'); - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-20,NOW()),0,"in"), - (?,TIMESTAMPADD(SECOND,-43199,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - error = e; - } - - expect(error.sqlMessage).toEqual('Descansos 12 h'); - }); - - it('should throw an error if the worker has finished the 12h break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - stmts.push('START TRANSACTION'); - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-20,NOW()),0,"in"), - (?,TIMESTAMPADD(SECOND,-43201,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err).not.toBeDefined(); - }); - - it('should throw an error if the worker has odd entry records', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - stmts.push('START TRANSACTION'); - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in")`, [ - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err.sqlMessage).toEqual('Dias con fichadas impares'); - }); - - it('should throw an error if the worker try to sign on a holiday day', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-20,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err.sqlMessage).toEqual('Holidays'); - }); - - it('should throw an error if the worker try to sign with your contract ended', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`UPDATE vn.business SET ended = DATE_ADD(CURDATE(), INTERVAL -1 DAY) WHERE id = ?`, [ - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-20,NOW()),0,"out")`, [ - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err.sqlMessage).toEqual('No hay un contrato en vigor'); - }); - - it('should throw an error if the worker has not finished the 36h weekly break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - - stmts.push('SET @warn := NULL'); - - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-16,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-48,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-40,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-72,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-64,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-96,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-88,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-120,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-112,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-144,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-136,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-168,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-160,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-192,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-184,NOW()),0,"out")`, [ - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - - let warningMessageIndex = stmts.push('SELECT @warn AS warning') - 1; - stmts.push('ROLLBACK'); - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await app.models.Worker.rawStmt(sql); - - expect(result[warningMessageIndex][0].warning).toEqual('Descansos 36 h'); - }); - - it('should check if the worker has finished the 36h weekly break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - - stmts.push('SET @warn := NULL'); - - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-16,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-48,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-40,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-72,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-64,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-96,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-88,NOW()),0,"out")`, [ - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - let warningMessageIndex = stmts.push('SELECT @warn AS warning') - 1; - - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await app.models.Worker.rawStmt(sql); - - expect(result[warningMessageIndex][0].warning).toBe(null); - }); - - it('should throw an error if the worker has not finished the 72h biweekly break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-16,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-48,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-40,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-72,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-64,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-96,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-88,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-120,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-112,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-144,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-136,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-168,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-160,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-192,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-184,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-216,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-208,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-240,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-232,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-264,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-256,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-289,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-280,NOW()),0,"out")`, [ - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - stmts.push('SELECT @warn AS warning') - 1; - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err.sqlMessage).toEqual('Descansos 72 h'); - }); - - it('should check if the worker has finished the 72h biweekly break', async() => { - const workerId = 1109; - const tabletId = 1; - let stmts = []; - let stmt; - let err; - stmts.push('START TRANSACTION'); - - stmt = new ParameterizedSQL(`INSERT INTO vn.workerTimeControl(userFk,timed,manual,direction) - VALUES - (?,TIMESTAMPADD(HOUR,-24,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-16,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-48,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-40,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-72,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-64,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-96,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-88,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-120,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-112,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-144,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-136,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-168,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-160,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-192,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-184,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-216,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-208,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-240,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-232,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-264,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-256,NOW()),0,"out"), - (?,TIMESTAMPADD(HOUR,-288,NOW()),0,"in"), - (?,TIMESTAMPADD(HOUR,-280,NOW()),0,"out")`, [ - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId, - workerId - ]); - stmts.push(stmt); - - stmt = new ParameterizedSQL('CALL vn.workerTimeControl_check(?, ?, NULL)', [ - workerId, - tabletId - ]); - stmts.push(stmt); - stmts.push('ROLLBACK'); - - stmts.push('SELECT @warn AS warning') - 1; - - let sql = ParameterizedSQL.join(stmts, ';'); - - try { - await app.models.Worker.rawStmt(sql); - } catch (e) { - await app.models.Worker.rawSql('ROLLBACK'); - err = e; - } - - expect(err).not.toBeDefined(); - }); -}); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 6f78d7c4e..cec0545a0 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -409,11 +409,11 @@ export default { inactiveIcon: 'vn-item-descriptor vn-icon[icon="icon-unavailable"]' }, itemRequest: { - firstRequestItemID: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td-editable:nth-child(7)', - firstRequestQuantity: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td-editable:nth-child(8)', - firstRequestConcept: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(9)', - firstRequestStatus: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(10)', - secondRequestStatus: 'vn-item-request vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(10)', + firstRequestItemID: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td-editable:nth-child(8)', + firstRequestQuantity: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td-editable:nth-child(9)', + firstRequestConcept: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(10)', + firstRequestStatus: 'vn-item-request vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(11)', + secondRequestStatus: 'vn-item-request vn-tbody > vn-tr:nth-child(2) > vn-td:nth-child(11)', secondRequestDecline: 'vn-item-request vn-tr:nth-child(2) vn-icon-button[icon="thumb_down"]', declineReason: 'vn-textarea[ng-model="$ctrl.denyObservation"]' }, @@ -1192,7 +1192,7 @@ export default { secondBuyPacking: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.packing"]', secondBuyWeight: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.weight"]', secondBuyStickers: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.stickers"]', - secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packageFk"]', + secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packagingFk"]', secondBuyQuantity: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.quantity"]', secondBuyItem: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.itemFk"]', importButton: 'vn-entry-buy-index vn-icon[icon="publish"]', diff --git a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js index 6264073f6..5e82306cc 100644 --- a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js +++ b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js @@ -134,14 +134,6 @@ describe('Ticket Edit sale path', () => { await page.accessToSection('ticket.card.sale'); }); - it('should try to add a higher quantity value and then receive an error', async() => { - await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell); - await page.type(selectors.ticketSales.firstSaleQuantity, '11\u000d'); - const message = await page.waitForSnackbar(); - - expect(message.text).toContain('The new quantity should be smaller than the old one'); - }); - it('should remove 1 from the first sale quantity', async() => { await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell); await page.waitForSelector(selectors.ticketSales.firstSaleQuantity); @@ -220,7 +212,7 @@ describe('Ticket Edit sale path', () => { it('should log in as salesAssistant and navigate to ticket sales', async() => { await page.loginAndModule('salesAssistant', 'ticket'); - await page.accessToSearchResult('17'); + await page.accessToSearchResult('15'); await page.accessToSection('ticket.card.sale'); }); @@ -324,7 +316,7 @@ describe('Ticket Edit sale path', () => { }); it('should confirm the transfered quantity is the correct one', async() => { - const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText'); + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleQuantityCell, 'innerText'); expect(result).toContain('20'); }); @@ -378,7 +370,7 @@ describe('Ticket Edit sale path', () => { await page.waitToClick(selectors.ticketSales.moveToNewTicketButton); const message = await page.waitForSnackbar(); - expect(message.text).toContain(`You can't create a ticket for a inactive client`); + expect(message.text).toContain(`You can't create a ticket for an inactive client`); await page.closePopup(); }); diff --git a/e2e/paths/13-supplier/03_fiscal_data.spec.js b/e2e/paths/13-supplier/03_fiscal_data.spec.js index 891b769c9..96977e708 100644 --- a/e2e/paths/13-supplier/03_fiscal_data.spec.js +++ b/e2e/paths/13-supplier/03_fiscal_data.spec.js @@ -1,20 +1,5 @@ import getBrowser from '../../helpers/puppeteer'; -const $ = { - saveButton: 'vn-supplier-fiscal-data button[type="submit"]', -}; -const $inputs = { - province: 'vn-supplier-fiscal-data [name="province"]', - country: 'vn-supplier-fiscal-data [name="country"]', - postcode: 'vn-supplier-fiscal-data [name="postcode"]', - city: 'vn-supplier-fiscal-data [name="city"]', - socialName: 'vn-supplier-fiscal-data [name="socialName"]', - taxNumber: 'vn-supplier-fiscal-data [name="taxNumber"]', - account: 'vn-supplier-fiscal-data [name="account"]', - sageWithholding: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageWithholdingFk"]', - sageTaxType: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageTaxTypeFk"]' -}; - describe('Supplier fiscal data path', () => { let browser; let page; @@ -30,7 +15,7 @@ describe('Supplier fiscal data path', () => { await browser.close(); }); - it('should attempt to edit the fiscal data and check data is saved', async() => { + it('should attempt to edit the fiscal data and check data iss saved', async() => { await page.accessToSection('supplier.card.fiscalData'); const form = 'vn-supplier-fiscal-data form'; @@ -40,16 +25,16 @@ describe('Supplier fiscal data path', () => { postcode: null, city: 'Valencia', socialName: 'Farmer King SL', - taxNumber: 'Wrong tax number', + taxNumber: '12345678Z', account: '0123456789', sageWithholding: 'retencion estimacion objetiva', sageTaxType: 'operaciones no sujetas' }; - const errorMessage = await page.sendForm(form, values); - const message = await page.sendForm(form, { - taxNumber: '12345678Z' + const errorMessage = await page.sendForm(form, { + taxNumber: 'Wrong tax number' }); + const message = await page.sendForm(form, values); await page.reloadSection('supplier.card.fiscalData'); const formValues = await page.fetchForm(form, Object.keys(values)); diff --git a/front/Dockerfile b/front/Dockerfile index 098ee161e..d0ee26904 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:stretch-slim +FROM debian:bookworm-slim EXPOSE 80 ENV TZ Europe/Madrid diff --git a/front/core/components/index.js b/front/core/components/index.js index eb40d5e1f..cfa46f0cb 100644 --- a/front/core/components/index.js +++ b/front/core/components/index.js @@ -39,6 +39,7 @@ import './range'; import './input-time'; import './input-file'; import './label'; +import './link-phone'; import './list'; import './popover'; import './popup'; diff --git a/front/core/components/link-phone/index.html b/front/core/components/link-phone/index.html new file mode 100644 index 000000000..2789ab75c --- /dev/null +++ b/front/core/components/link-phone/index.html @@ -0,0 +1,14 @@ + + {{$ctrl.phoneNumber}} + + + + + +- diff --git a/front/core/components/link-phone/index.js b/front/core/components/link-phone/index.js new file mode 100644 index 000000000..6f3eec5b1 --- /dev/null +++ b/front/core/components/link-phone/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import './style.scss'; +class Controller { + constructor() { + this.phoneNumber = null; + } +} + +ngModule.vnComponent('vnLinkPhone', { + template: require('./index.html'), + controller: Controller, + bindings: { + phoneNumber: '<', + } +}); diff --git a/front/core/components/link-phone/style.scss b/front/core/components/link-phone/style.scss new file mode 100644 index 000000000..22beaf8fd --- /dev/null +++ b/front/core/components/link-phone/style.scss @@ -0,0 +1,7 @@ +vn-link-phone { + vn-icon { + font-size: 1.1em; + vertical-align: bottom; +} +} + diff --git a/loopback/common/models/application.json b/loopback/common/models/application.json index bc72df315..f79001585 100644 --- a/loopback/common/models/application.json +++ b/loopback/common/models/application.json @@ -13,6 +13,6 @@ "principalType": "ROLE", "principalId": "$everyone", "permission": "ALLOW" - } + } ] } diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 645a874e8..26650175d 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -14,7 +14,7 @@ "The default consignee can not be unchecked": "The default consignee can not be unchecked", "Enter an integer different to zero": "Enter an integer different to zero", "Package cannot be blank": "Package cannot be blank", - "The new quantity should be smaller than the old one": "The new quantity should be smaller than the old one", + "The price of the item changed": "The price of the item changed", "The sales of this ticket can't be modified": "The sales of this ticket can't be modified", "Cannot check Equalization Tax in this NIF/CIF": "Cannot check Equalization Tax in this NIF/CIF", "You can't create an order for a frozen client": "You can't create an order for a frozen client", @@ -23,7 +23,7 @@ "Agency cannot be blank": "Agency cannot be blank", "The IBAN does not have the correct format": "The IBAN does not have the correct format", "You can't make changes on the basic data of an confirmed order or with rows": "You can't make changes on the basic data of an confirmed order or with rows", - "You can't create a ticket for a inactive client": "You can't create a ticket for a inactive client", + "You can't create a ticket for an inactive client": "You can't create a ticket for an inactive client", "Worker cannot be blank": "Worker cannot be blank", "You must delete the claim id %d first": "You must delete the claim id %d first", "You don't have enough privileges": "You don't have enough privileges", @@ -188,6 +188,14 @@ "The ticket doesn't exist.": "The ticket doesn't exist.", "The sales do not exists": "The sales do not exists", "Ticket without Route": "Ticket without route", + "Select a different client": "Select a different client", + "Fill all the fields": "Fill all the fields", + "Error while generating PDF": "Error while generating PDF", + "Can't invoice to future": "Can't invoice to future", + "This ticket is already invoiced": "This ticket is already invoiced", + "Negative basis of tickets: 23": "Negative basis of tickets: 23", "Booking completed": "Booking complete", - "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation" + "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation", + "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets" } + diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 6e478c000..b42720458 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -5,10 +5,10 @@ "The default consignee can not be unchecked": "No se puede desmarcar el consignatario predeterminado", "Unable to default a disabled consignee": "No se puede poner predeterminado un consignatario desactivado", "Can't be blank": "No puede estar en blanco", - "Invalid TIN": "NIF/CIF invalido", + "Invalid TIN": "NIF/CIF inválido", "TIN must be unique": "El NIF/CIF debe ser único", "A client with that Web User name already exists": "Ya existe un cliente con ese Usuario Web", - "Is invalid": "Is invalid", + "Is invalid": "Es inválido", "Quantity cannot be zero": "La cantidad no puede ser cero", "Enter an integer different to zero": "Introduce un entero distinto de cero", "Package cannot be blank": "El embalaje no puede estar en blanco", @@ -35,7 +35,7 @@ "The grade must be an integer greater than or equal to zero": "El grade debe ser un entero mayor o igual a cero", "Sample type cannot be blank": "El tipo de plantilla no puede quedar en blanco", "Description cannot be blank": "Se debe rellenar el campo de texto", - "The new quantity should be smaller than the old one": "La nueva cantidad debe de ser menor que la anterior", + "The price of the item changed": "El precio del artículo cambió", "The value should not be greater than 100%": "El valor no debe de ser mayor de 100%", "The value should be a number": "El valor debe ser un numero", "This order is not editable": "Esta orden no se puede modificar", @@ -55,17 +55,17 @@ "You must delete the claim id %d first": "Antes debes borrar la reclamación %d", "You don't have enough privileges": "No tienes suficientes permisos", "Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF", - "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos", - "INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no esta permitido el uso de la letra ñ", + "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos básicos de una orden con artículos", + "INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no está permitido el uso de la letra ñ", "You can't create a ticket for a frozen client": "No puedes crear un ticket para un cliente congelado", - "You can't create a ticket for a inactive client": "No puedes crear un ticket para un cliente inactivo", + "You can't create a ticket for an inactive client": "No puedes crear un ticket para un cliente inactivo", "Tag value cannot be blank": "El valor del tag no puede quedar en blanco", "ORDER_EMPTY": "Cesta vacía", "You don't have enough privileges to do that": "No tienes permisos para cambiar esto", "NO SE PUEDE DESACTIVAR EL CONSIGNAT": "NO SE PUEDE DESACTIVAR EL CONSIGNAT", "Error. El NIF/CIF está repetido": "Error. El NIF/CIF está repetido", "Street cannot be empty": "Dirección no puede estar en blanco", - "City cannot be empty": "Cuidad no puede estar en blanco", + "City cannot be empty": "Ciudad no puede estar en blanco", "Code cannot be blank": "Código no puede estar en blanco", "You cannot remove this department": "No puedes eliminar este departamento", "The extension must be unique": "La extensión debe ser unica", @@ -102,8 +102,8 @@ "You can't delete a confirmed order": "No puedes borrar un pedido confirmado", "The social name has an invalid format": "El nombre fiscal tiene un formato incorrecto", "Invalid quantity": "Cantidad invalida", - "This postal code is not valid": "This postal code is not valid", - "is invalid": "is invalid", + "This postal code is not valid": "Este código postal no es válido", + "is invalid": "es inválido", "The postcode doesn't exist. Please enter a correct one": "El código postal no existe. Por favor, introduce uno correcto", "The department name can't be repeated": "El nombre del departamento no puede repetirse", "This phone already exists": "Este teléfono ya existe", @@ -112,8 +112,8 @@ "You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada", "You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero", "You should specify a date": "Debes especificar una fecha", - "You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fín", - "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín", + "You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fin", + "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fin", "You should mark at least one week day": "Debes marcar al menos un día de la semana", "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", @@ -144,15 +144,15 @@ "Unable to clone this travel": "No ha sido posible clonar este travel", "This thermograph id already exists": "La id del termógrafo ya existe", "Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante", - "ORDER_ALREADY_CONFIRMED": "ORDER_ALREADY_CONFIRMED", + "ORDER_ALREADY_CONFIRMED": "ORDEN YA CONFIRMADA", "Invalid password": "Invalid password", "Password does not meet requirements": "La contraseña no cumple los requisitos", - "Role already assigned": "Role already assigned", - "Invalid role name": "Invalid role name", - "Role name must be written in camelCase": "Role name must be written in camelCase", - "Email already exists": "Email already exists", - "User already exists": "User already exists", - "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", + "Role already assigned": "Rol ya asignado", + "Invalid role name": "Nombre de rol no válido", + "Role name must be written in camelCase": "El nombre del rol debe escribirse en camelCase", + "Email already exists": "El correo ya existe", + "User already exists": "El/La usuario/a ya existe", + "Absence change notification on the labour calendar": "Notificación de cambio de ausencia en el calendario laboral", "Record of hours week": "Registro de horas semana {{week}} año {{year}} ", "Created absence": "El empleado {{author}} ha añadido una ausencia de tipo '{{absenceType}}' a {{employee}} para el día {{dated}}.", "Deleted absence": "El empleado {{author}} ha eliminado una ausencia de tipo '{{absenceType}}' a {{employee}} del día {{dated}}.", @@ -317,8 +317,13 @@ "The ticket doesn't exist.": "No existe el ticket.", "Social name should be uppercase": "La razón social debe ir en mayúscula", "Street should be uppercase": "La dirección fiscal debe ir en mayúscula", - "The response is not a PDF": "La respuesta no es un PDF", "Ticket without Route": "Ticket sin ruta", + "Select a different client": "Seleccione un cliente distinto", + "Fill all the fields": "Rellene todos los campos", + "The response is not a PDF": "La respuesta no es un PDF", "Booking completed": "Reserva completada", - "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación" + "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación", + "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina", + "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina", + "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada" } diff --git a/modules/account/back/models/mail-forward.json b/modules/account/back/models/mail-forward.json index edef1bf08..874810b7a 100644 --- a/modules/account/back/models/mail-forward.json +++ b/modules/account/back/models/mail-forward.json @@ -21,5 +21,16 @@ "model": "VnUser", "foreignKey": "account" } - } + }, + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW" + }, { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW" + }] } diff --git a/modules/account/front/basic-data/index.html b/modules/account/front/basic-data/index.html index 6f757753e..9fd3506fe 100644 --- a/modules/account/front/basic-data/index.html +++ b/modules/account/front/basic-data/index.html @@ -1,9 +1,9 @@ + + form="form" + save="patch">
diff --git a/modules/account/front/basic-data/index.js b/modules/account/front/basic-data/index.js index 77d3eab26..f6b266bbc 100644 --- a/modules/account/front/basic-data/index.js +++ b/modules/account/front/basic-data/index.js @@ -18,5 +18,8 @@ ngModule.component('vnUserBasicData', { controller: Controller, require: { card: '^vnUserCard' + }, + bindings: { + user: '<' } }); diff --git a/modules/account/front/routes.json b/modules/account/front/routes.json index fd33e7122..d7845090b 100644 --- a/modules/account/front/routes.json +++ b/modules/account/front/routes.json @@ -78,7 +78,9 @@ "state": "account.card.basicData", "component": "vn-user-basic-data", "description": "Basic data", - "acl": ["itManagement"] + "params": { + "user": "$ctrl.user" + } }, { "url" : "/log", diff --git a/modules/claim/back/methods/claim/claimPickupEmail.js b/modules/claim/back/methods/claim/claimPickupEmail.js index 1fd8eb99e..596102a24 100644 --- a/modules/claim/back/methods/claim/claimPickupEmail.js +++ b/modules/claim/back/methods/claim/claimPickupEmail.js @@ -43,9 +43,8 @@ module.exports = Self => { Self.claimPickupEmail = async ctx => { const models = Self.app.models; - const userId = ctx.req.accessToken.userId; const $t = ctx.req.__; // $translate - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const args = Object.assign({}, ctx.args); const params = { @@ -70,9 +69,8 @@ module.exports = Self => { const message = $t('Claim pickup order sent', { claimId: args.id, clientName: claim.client().name, - claimUrl: `${origin}/#!/claim/${args.id}/summary`, + claimUrl: `${url}claim/${args.id}/summary`, }); - const salesPersonId = claim.client().salesPersonFk; if (salesPersonId) await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); diff --git a/modules/claim/back/methods/claim/createFromSales.js b/modules/claim/back/methods/claim/createFromSales.js index 07bdb30aa..30093e43d 100644 --- a/modules/claim/back/methods/claim/createFromSales.js +++ b/modules/claim/back/methods/claim/createFromSales.js @@ -94,13 +94,13 @@ module.exports = Self => { const salesPerson = ticket.client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Created claim', { claimId: newClaim.id, ticketId: ticketId, - ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, - claimUrl: `${origin}/#!/claim/${newClaim.id}/summary`, + ticketUrl: `${url}ticket/${ticketId}/sale`, + claimUrl: `${url}claim/${newClaim.id}/summary`, changes: changesMade }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); diff --git a/modules/claim/back/methods/claim/regularizeClaim.js b/modules/claim/back/methods/claim/regularizeClaim.js index 672c94947..a51b114ef 100644 --- a/modules/claim/back/methods/claim/regularizeClaim.js +++ b/modules/claim/back/methods/claim/regularizeClaim.js @@ -56,15 +56,15 @@ module.exports = Self => { const salesPerson = sale.ticket().client().salesPersonUser(); if (salesPerson) { const nickname = address && address.nickname || destination.description; - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Sent units from ticket', { quantity: sale.quantity, concept: sale.concept, itemId: sale.itemFk, ticketId: sale.ticketFk, nickname: nickname, - ticketUrl: `${origin}/#!/ticket/${sale.ticketFk}/sale`, - itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + ticketUrl: `${url}ticket/${sale.ticketFk}/sale`, + itemUrl: `${url}item/${sale.itemFk}/summary` }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); } diff --git a/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js b/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js index 276843c32..95c356374 100644 --- a/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js +++ b/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js @@ -64,7 +64,7 @@ describe('claim regularizeClaim()', () => { claimEnds = await importTicket(ticketId, claimId, userId, options); - for (claimEnd of claimEnds) + for (const claimEnd of claimEnds) await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options); let claimBefore = await models.Claim.findById(claimId, null, options); diff --git a/modules/claim/back/methods/claim/specs/updateClaim.spec.js b/modules/claim/back/methods/claim/specs/updateClaim.spec.js index d367fb89f..85ada869a 100644 --- a/modules/claim/back/methods/claim/specs/updateClaim.spec.js +++ b/modules/claim/back/methods/claim/specs/updateClaim.spec.js @@ -2,7 +2,9 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); describe('Update Claim', () => { + let url; beforeAll(async() => { + url = await app.models.Url.getUrl(); const activeCtx = { accessToken: {userId: 9}, http: { @@ -29,7 +31,6 @@ describe('Update Claim', () => { it(`should throw an error as the user doesn't have rights`, async() => { const tx = await app.models.Claim.beginTransaction({}); - let error; try { @@ -77,7 +78,7 @@ describe('Update Claim', () => { const ctx = { req: { accessToken: {userId: claimManagerId}, - headers: {origin: 'http://localhost'} + headers: {origin: url} }, args: { observation: 'valid observation', @@ -118,7 +119,7 @@ describe('Update Claim', () => { const ctx = { req: { accessToken: {userId: claimManagerId}, - headers: {origin: 'http://localhost'} + headers: {origin: url} }, args: { observation: 'valid observation', diff --git a/modules/claim/back/methods/claim/updateClaim.js b/modules/claim/back/methods/claim/updateClaim.js index 5486b8eff..d99528413 100644 --- a/modules/claim/back/methods/claim/updateClaim.js +++ b/modules/claim/back/methods/claim/updateClaim.js @@ -91,16 +91,16 @@ module.exports = Self => { // When hasToPickUp has been changed if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp) - notifyPickUp(ctx, salesPerson.id, claim); + await notifyPickUp(ctx, salesPerson.id, claim); // When claimState has been changed if (args.claimStateFk) { const newState = await models.ClaimState.findById(args.claimStateFk, null, myOptions); if (newState.hasToNotify) { if (newState.code == 'incomplete') - notifyStateChange(ctx, salesPerson.id, claim, newState.code); + await notifyStateChange(ctx, salesPerson.id, claim, newState.code); if (newState.code == 'canceled') - notifyStateChange(ctx, claim.workerFk, claim, newState.code); + await notifyStateChange(ctx, claim.workerFk, claim, newState.code); } } @@ -115,26 +115,26 @@ module.exports = Self => { async function notifyStateChange(ctx, workerId, claim, state) { const models = Self.app.models; - const origin = ctx.req.headers.origin; + const url = await models.Url.getUrl(); const $t = ctx.req.__; // $translate const message = $t(`Claim state has changed to ${state}`, { claimId: claim.id, clientName: claim.client().name, - claimUrl: `${origin}/#!/claim/${claim.id}/summary` + claimUrl: `${url}claim/${claim.id}/summary` }); await models.Chat.sendCheckingPresence(ctx, workerId, message); } async function notifyPickUp(ctx, workerId, claim) { - const origin = ctx.req.headers.origin; const models = Self.app.models; + const url = await models.Url.getUrl(); const $t = ctx.req.__; // $translate const message = $t('Claim will be picked', { claimId: claim.id, clientName: claim.client().name, - claimUrl: `${origin}/#!/claim/${claim.id}/summary` + claimUrl: `${url}claim/${claim.id}/summary` }); await models.Chat.sendCheckingPresence(ctx, workerId, message); } diff --git a/modules/claim/front/locale/es.yml b/modules/claim/front/locale/es.yml index f6dac2b83..83ccf1e7b 100644 --- a/modules/claim/front/locale/es.yml +++ b/modules/claim/front/locale/es.yml @@ -17,6 +17,7 @@ Search claim by id or client name: Buscar reclamaciones por identificador o nomb Claim deleted!: Reclamación eliminada! claim: reclamación Photos: Fotos +Development: Trazabilidad Go to the claim: Ir a la reclamación Sale tracking: Líneas preparadas Ticket tracking: Estados del ticket diff --git a/modules/client/back/methods/client/incotermsAuthorizationEmail.js b/modules/client/back/methods/client/incotermsAuthorizationEmail.js index 4a21f20b0..9b3686b07 100644 --- a/modules/client/back/methods/client/incotermsAuthorizationEmail.js +++ b/modules/client/back/methods/client/incotermsAuthorizationEmail.js @@ -33,6 +33,12 @@ module.exports = Self => { type: 'number', description: 'The company id', required: true + }, + { + arg: 'addressId', + type: 'number', + description: 'The address id', + required: true } ], returns: { diff --git a/modules/client/back/methods/client/incotermsAuthorizationHtml.js b/modules/client/back/methods/client/incotermsAuthorizationHtml.js index 0a6bba0a8..af585b1ae 100644 --- a/modules/client/back/methods/client/incotermsAuthorizationHtml.js +++ b/modules/client/back/methods/client/incotermsAuthorizationHtml.js @@ -21,6 +21,12 @@ module.exports = Self => { type: 'number', description: 'The company id', required: true + }, + { + arg: 'addressId', + type: 'number', + description: 'The address id', + required: true } ], returns: [ diff --git a/modules/client/back/methods/client/incotermsAuthorizationPdf.js b/modules/client/back/methods/client/incotermsAuthorizationPdf.js index d37e473f1..ffe17c72f 100644 --- a/modules/client/back/methods/client/incotermsAuthorizationPdf.js +++ b/modules/client/back/methods/client/incotermsAuthorizationPdf.js @@ -21,6 +21,12 @@ module.exports = Self => { type: 'number', description: 'The company id', required: true + }, + { + arg: 'addressId', + type: 'number', + description: 'The address id', + required: true } ], returns: [ diff --git a/modules/client/back/methods/client/specs/transactions.spec.js b/modules/client/back/methods/client/specs/transactions.spec.js index 45fffc1de..54e088929 100644 --- a/modules/client/back/methods/client/specs/transactions.spec.js +++ b/modules/client/back/methods/client/specs/transactions.spec.js @@ -1,15 +1,24 @@ const models = require('vn-loopback/server/server').models; describe('Client transactions', () => { + const ctx = {}; + it('should call transactions() method to receive a list of Web Payments from BRUCE WAYNE', async() => { const tx = await models.Client.beginTransaction({}); - try { const options = {transaction: tx}; - const ctx = {}; const filter = {where: {clientFk: 1101}}; - const result = await models.Client.transactions(ctx, filter, options); + const result = await models.Client.transactions( + ctx, + filter, + undefined, + undefined, + undefined, + undefined, + undefined, + options + ); expect(result[1].id).toBeTruthy(); @@ -26,9 +35,17 @@ describe('Client transactions', () => { try { const options = {transaction: tx}; - const ctx = {args: {orderFk: 6}}; const filter = {}; - const result = await models.Client.transactions(ctx, filter, options); + const result = await models.Client.transactions( + ctx, + filter, + 6, + undefined, + undefined, + undefined, + undefined, + options + ); const firstRow = result[0]; @@ -47,10 +64,17 @@ describe('Client transactions', () => { try { const options = {transaction: tx}; - - const ctx = {args: {amount: 40}}; const filter = {}; - const result = await models.Client.transactions(ctx, filter, options); + const result = await models.Client.transactions( + ctx, + filter, + undefined, + undefined, + 40, + undefined, + undefined, + options + ); const randomIndex = Math.floor(Math.random() * result.length); const transaction = result[randomIndex]; @@ -63,4 +87,43 @@ describe('Client transactions', () => { throw e; } }); + + it('should call transactions() method filtering by date', async() => { + const tx = await models.Client.beginTransaction({}); + + try { + const options = {transaction: tx}; + const filter = {}; + const withResults = await models.Client.transactions( + ctx, + filter, + undefined, + undefined, + undefined, + '2000/12/31', + undefined, + options + ); + + expect(withResults.length).toEqual(6); + + const noResults = await models.Client.transactions( + ctx, + filter, + undefined, + undefined, + undefined, + '2099/12/31', + undefined, + options + ); + + expect(noResults.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/client/back/methods/client/transactions.js b/modules/client/back/methods/client/transactions.js index 691910721..a643d69f4 100644 --- a/modules/client/back/methods/client/transactions.js +++ b/modules/client/back/methods/client/transactions.js @@ -12,22 +12,26 @@ module.exports = Self => { arg: 'filter', type: 'object', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', - http: {source: 'query'} }, { arg: 'orderFk', type: 'number', - http: {source: 'query'} }, { arg: 'clientFk', type: 'number', - http: {source: 'query'} }, { arg: 'amount', type: 'number', - http: {source: 'query'} + }, + { + arg: 'from', + type: 'date', + }, + { + arg: 'to', + type: 'date', } ], returns: { @@ -40,14 +44,15 @@ module.exports = Self => { } }); - Self.transactions = async(ctx, filter, options) => { - const args = ctx.args; + Self.transactions = async(ctx, filter, orderFk, clientFk, amount, from, to, options) => { const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - const where = buildFilter(args, (param, value) => { + if (to) to.setHours(23, 59, 59, 999); + + const where = buildFilter({orderFk, clientFk, amount, from, to}, (param, value) => { switch (param) { case 'orderFk': return {'t.id': value}; @@ -55,6 +60,10 @@ module.exports = Self => { return {'t.clientFk': value}; case 'amount': return {'t.amount': (value * 100)}; + case 'from': + return {'t.created': {gte: value}}; + case 'to': + return {'t.created': {lte: value}}; } }); diff --git a/modules/client/back/model-config.json b/modules/client/back/model-config.json index 296b5e6a7..0cc5df9a2 100644 --- a/modules/client/back/model-config.json +++ b/modules/client/back/model-config.json @@ -5,6 +5,9 @@ "AddressObservation": { "dataSource": "vn" }, + "AddressShortage": { + "dataSource": "vn" + }, "BankEntity": { "dataSource": "vn" }, diff --git a/modules/client/back/models/addressShortage.json b/modules/client/back/models/addressShortage.json new file mode 100644 index 000000000..1ae8d986c --- /dev/null +++ b/modules/client/back/models/addressShortage.json @@ -0,0 +1,22 @@ +{ + "name": "AddressShortage", + "base": "VnModel", + "options": { + "mysql": { + "table": "addressShortage" + } + }, + "properties": { + "addressFk": { + "type": "number", + "id": true + } + }, + "relations": { + "address": { + "type": "belongsTo", + "model": "Address", + "foreignKey": "addressFk" + } + } +} \ No newline at end of file diff --git a/modules/client/back/models/client-config.json b/modules/client/back/models/client-config.json index 90d47333d..6c5eae7d7 100644 --- a/modules/client/back/models/client-config.json +++ b/modules/client/back/models/client-config.json @@ -17,6 +17,9 @@ }, "maxCreditRows": { "type": "number" + }, + "defaultCredit": { + "type": "number" } } } \ No newline at end of file diff --git a/modules/client/back/models/client.js b/modules/client/back/models/client.js index 8e720484f..72b702779 100644 --- a/modules/client/back/models/client.js +++ b/modules/client/back/models/client.js @@ -250,7 +250,12 @@ module.exports = Self => { const loopBackContext = LoopBackContext.getCurrentContext(); const accessToken = {req: loopBackContext.active.accessToken}; - const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl(accessToken, 'Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE'); + const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl( + accessToken, + 'Client', + 'editVerifiedDataWithoutTaxDataCheck', + 'WRITE' + ); const hasChanges = orgData && changes; const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); @@ -263,7 +268,9 @@ module.exports = Self => { const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked; - const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !editVerifiedDataWithoutTaxDataChecked; + const cantChangeSageData = (sageTaxTypeChanged || + sageTransactionTypeChanged + ) && !editVerifiedDataWithoutTaxDataChecked; if (cantEditVerifiedData || cantChangeSageData) throw new UserError(`You don't have enough privileges`); @@ -346,8 +353,7 @@ module.exports = Self => { const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const $t = httpRequest.__; - const headers = httpRequest.headers; - const origin = headers.origin; + const url = await Self.app.models.Url.getUrl(); const salesPersonId = instance.salesPersonFk; @@ -366,7 +372,7 @@ module.exports = Self => { await email.send(); } - const fullUrl = `${origin}/#!/client/${instance.id}/billing-data`; + const fullUrl = `${url}client/${instance.id}/billing-data`; const message = $t('Changed client paymethod', { clientId: instance.id, clientName: instance.name, @@ -389,8 +395,7 @@ module.exports = Self => { const httpCtx = {req: loopBackContext.active}; const httpRequest = httpCtx.req.http.req; const $t = httpRequest.__; - const headers = httpRequest.headers; - const origin = headers.origin; + const url = await Self.app.models.Url.getUrl(); const models = Self.app.models; let previousWorker = {name: $t('None')}; @@ -411,7 +416,7 @@ module.exports = Self => { currentWorker.name = worker && worker.user().nickname; } - const fullUrl = `${origin}/#!/client/${client.id}/basic-data`; + const fullUrl = `${url}client/${client.id}/basic-data`; const message = $t('Client assignment has changed', { clientId: client.id, clientName: client.name, @@ -445,14 +450,14 @@ module.exports = Self => { if (lastCredit && lastCredit.amount == 0) { const zeroCreditEditor = - await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE'); + await models.ACL.checkAccessAcl(accessToken, 'Client', 'zeroCreditEditor', 'WRITE'); const lastCreditIsNotEditable = - await models.ACL.checkAccessAcl( - {req: {accessToken: {userId: lastCredit.workerFk}}}, - 'Client', - 'zeroCreditEditor', - 'WRITE' - ); + await models.ACL.checkAccessAcl( + {req: {accessToken: {userId: lastCredit.workerFk}}}, + 'Client', + 'zeroCreditEditor', + 'WRITE' + ); if (lastCreditIsNotEditable && !zeroCreditEditor) throw new UserError(`You can't change the credit set to zero from a financialBoss`); @@ -478,12 +483,6 @@ module.exports = Self => { if (userRequiredRoles <= 0) throw new UserError(`You don't have enough privileges to set this credit amount`); } - - await models.ClientCredit.create({ - amount: changes.credit, - clientFk: finalState.id, - workerFk: userId - }, ctx.options); }; Self.changeCreditManagement = async function changeCreditManagement(ctx, finalState, changes) { diff --git a/modules/client/back/models/client.json b/modules/client/back/models/client.json index 66a67ec2e..f32915bb5 100644 --- a/modules/client/back/models/client.json +++ b/modules/client/back/models/client.json @@ -158,7 +158,7 @@ }, "user": { "type": "belongsTo", - "model": "Account", + "model": "VnUser", "foreignKey": "id" }, "payMethod": { diff --git a/modules/client/back/models/credit-insurance.js b/modules/client/back/models/credit-insurance.js index 6f656d382..84bd90424 100644 --- a/modules/client/back/models/credit-insurance.js +++ b/modules/client/back/models/credit-insurance.js @@ -57,8 +57,8 @@ module.exports = function(Self) { const httpRequest = httpCtx.req.http.req; const $t = httpRequest.__; - const origin = httpRequest.headers.origin; - const fullPath = `${origin}/#!/client/${client.id}/credit-insurance/index`; + const url = await Self.app.models.Url.getUrl(); + const fullPath = `${url}client/${client.id}/credit-insurance/index`; const message = $t('MESSAGE_INSURANCE_CHANGE', { clientId: client.id, clientName: client.name, diff --git a/modules/client/back/models/specs/client.spec.js b/modules/client/back/models/specs/client.spec.js index 201d14bb4..bf134fbf9 100644 --- a/modules/client/back/models/specs/client.spec.js +++ b/modules/client/back/models/specs/client.spec.js @@ -62,13 +62,13 @@ describe('Client Model', () => { const options = {transaction: tx}; const ctx = {options}; - // Set credit to zero by a financialBoss const financialBoss = await models.VnUser.findOne({ where: {name: 'financialBoss'} }, options); ctx.options.accessToken = {userId: financialBoss.id}; - await models.Client.changeCredit(ctx, instance, {credit: 0}); + const testClient = await models.Client.findById(instance.id, options); + await testClient.updateAttributes({credit: 0}, ctx.options); const salesAssistant = await models.VnUser.findOne({ where: {name: 'salesAssistant'} diff --git a/modules/client/front/sample/create/index.html b/modules/client/front/sample/create/index.html index 5df2b29ef..9dce7ec72 100644 --- a/modules/client/front/sample/create/index.html +++ b/modules/client/front/sample/create/index.html @@ -8,11 +8,12 @@ auto-load="true" url="Samples/visible" fields="[ - 'id', + 'id', 'code', - 'description', + 'description', 'model', 'hasCompany', + 'hasAddress', 'hasPreview', 'datepickerEnabled' ]" @@ -65,10 +66,36 @@ model="ClientSample.companyFk" data="companiesData" show-field="code" - label="Company" + label="Company" ng-if="sampleType.selection.hasCompany" required="true"> + + + {{::!isActive ? '(Inactive)' : ''}} + {{::nickname}} + + , {{::street}}, {{::city}}, {{::province.name}} - {{::agencyMode.name}} + + + + + + + { + if (res.data) + this.addresses = res.data; + }); + } + + getClientDefaultAddress(value) { + let query = `Clients/${value}`; + this.$http.get(query).then(res => { + if (res.data) + this.addressId = res.data.defaultAddressFk; + }); + } } Controller.$inject = ['$element', '$scope', 'vnEmail']; diff --git a/modules/client/front/summary/index.html b/modules/client/front/summary/index.html index c622913bb..a42e192d0 100644 --- a/modules/client/front/summary/index.html +++ b/modules/client/front/summary/index.html @@ -45,11 +45,18 @@ - + + + + - + + + diff --git a/modules/entry/back/locale/buy/en.yml b/modules/entry/back/locale/buy/en.yml index 2db7c7be5..949aa8ed0 100644 --- a/modules/entry/back/locale/buy/en.yml +++ b/modules/entry/back/locale/buy/en.yml @@ -15,4 +15,4 @@ columns: weight: weight entryFk: entry itemFk: item - packageFk: package + packagingFk: package diff --git a/modules/entry/back/locale/buy/es.yml b/modules/entry/back/locale/buy/es.yml index 666bf7640..2aa7688eb 100644 --- a/modules/entry/back/locale/buy/es.yml +++ b/modules/entry/back/locale/buy/es.yml @@ -15,4 +15,4 @@ columns: weight: peso entryFk: entrada itemFk: artículo - packageFk: paquete + packagingFk: paquete diff --git a/modules/entry/back/methods/entry/addFromBuy.js b/modules/entry/back/methods/entry/addFromBuy.js index 82645b166..307c04b97 100644 --- a/modules/entry/back/methods/entry/addFromBuy.js +++ b/modules/entry/back/methods/entry/addFromBuy.js @@ -80,7 +80,7 @@ module.exports = Self => { comissionValue: buyUltimate.comissionValue, packageValue: buyUltimate.packageValue, location: buyUltimate.location, - packageFk: buyUltimate.packageFk, + packagingFk: buyUltimate.packagingFk, price1: buyUltimate.price1, price2: buyUltimate.price2, price3: buyUltimate.price3, diff --git a/modules/entry/back/methods/entry/getBuys.js b/modules/entry/back/methods/entry/getBuys.js index ebb271df1..90c1bb9d0 100644 --- a/modules/entry/back/methods/entry/getBuys.js +++ b/modules/entry/back/methods/entry/getBuys.js @@ -44,7 +44,7 @@ module.exports = Self => { 'grouping', 'groupingMode', 'quantity', - 'packageFk', + 'packagingFk', 'weight', 'buyingValue', 'price2', diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js index 1e6f01a5e..812775a1b 100644 --- a/modules/entry/back/methods/entry/importBuys.js +++ b/modules/entry/back/methods/entry/importBuys.js @@ -108,7 +108,7 @@ module.exports = Self => { packing: buy.packing, grouping: buy.grouping, buyingValue: buy.buyingValue, - packageFk: buy.packageFk, + packagingFk: buy.packagingFk, groupingMode: lastBuy.groupingMode, weight: lastBuy.weight }); diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js index 730ecab0a..de8fa732d 100644 --- a/modules/entry/back/methods/entry/importBuysPreview.js +++ b/modules/entry/back/methods/entry/importBuysPreview.js @@ -39,7 +39,7 @@ module.exports = Self => { }, myOptions); if (packaging) - buy.packageFk = packaging.id; + buy.packagingFk = packaging.id; const reference = await models.ItemMatchProperties.findOne({ fields: ['itemFk'], diff --git a/modules/entry/back/methods/entry/latestBuysFilter.js b/modules/entry/back/methods/entry/latestBuysFilter.js index 30d47a2a3..dbf7d2b17 100644 --- a/modules/entry/back/methods/entry/latestBuysFilter.js +++ b/modules/entry/back/methods/entry/latestBuysFilter.js @@ -153,64 +153,63 @@ module.exports = Self => { const date = Date.vnNew(); date.setHours(0, 0, 0, 0); stmt = new ParameterizedSQL(` - SELECT - i.image, - i.id AS itemFk, - i.size, - i.weightByPiece, - it.code, - i.typeFk, - i.family, - i.isActive, - i.minPrice, - i.description, - i.name, - i.subName, - i.packingOut, - i.tag5, - i.value5, - i.tag6, - i.value6, - i.tag7, - i.value7, - i.tag8, - i.value8, - i.tag9, - i.value9, - i.tag10, - i.value10, - t.name AS type, - intr.description AS intrastat, - ori.code AS origin, - b.entryFk, - b.id, - b.quantity, - b.buyingValue, - b.freightValue, - b.isIgnored, - b.packing, - b.grouping, - b.groupingMode, - b.comissionValue, - b.packageValue, - b.price2, - b.price3, - b.ektFk, - b.weight, - b.packageFk, - lb.landing - FROM cache.last_buy lb - LEFT JOIN cache.visible v ON v.item_id = lb.item_id - AND v.calc_id = @calc_id - JOIN item i ON i.id = lb.item_id - JOIN itemType it ON it.id = i.typeFk AND lb.warehouse_id = ? - JOIN buy b ON b.id = lb.buy_id - LEFT JOIN itemCategory ic ON ic.id = it.categoryFk - LEFT JOIN itemType t ON t.id = i.typeFk - LEFT JOIN intrastat intr ON intr.id = i.intrastatFk - LEFT JOIN origin ori ON ori.id = i.originFk - LEFT JOIN entry e ON e.id = b.entryFk AND e.created >= DATE_SUB(? ,INTERVAL 1 YEAR) - LEFT JOIN supplier s ON s.id = e.supplierFk` + SELECT i.image, + i.id AS itemFk, + i.size, + i.weightByPiece, + it.code, + i.typeFk, + i.family, + i.isActive, + i.minPrice, + i.description, + i.name, + i.subName, + i.packingOut, + i.tag5, + i.value5, + i.tag6, + i.value6, + i.tag7, + i.value7, + i.tag8, + i.value8, + i.tag9, + i.value9, + i.tag10, + i.value10, + t.name AS type, + intr.description AS intrastat, + ori.code AS origin, + b.entryFk, + b.id, + b.quantity, + b.buyingValue, + b.freightValue, + b.isIgnored, + b.packing, + b.grouping, + b.groupingMode, + b.comissionValue, + b.packageValue, + b.price2, + b.price3, + b.ektFk, + b.weight, + b.packagingFk, + lb.landing + FROM cache.last_buy lb + LEFT JOIN cache.visible v ON v.item_id = lb.item_id + AND v.calc_id = @calc_id + JOIN item i ON i.id = lb.item_id + JOIN itemType it ON it.id = i.typeFk AND lb.warehouse_id = ? + JOIN buy b ON b.id = lb.buy_id + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN itemType t ON t.id = i.typeFk + LEFT JOIN intrastat intr ON intr.id = i.intrastatFk + LEFT JOIN origin ori ON ori.id = i.originFk + LEFT JOIN entry e ON e.id = b.entryFk AND e.created >= DATE_SUB(? ,INTERVAL 1 YEAR) + LEFT JOIN supplier s ON s.id = e.supplierFk` , [userConfig.warehouseFk, date]); if (ctx.args.tags) { diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js index 4f9204c6a..a352294b9 100644 --- a/modules/entry/back/methods/entry/specs/importBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js @@ -33,7 +33,7 @@ describe('entry import()', () => { packing: 1, size: 1, volume: 1200, - packageFk: '94' + packagingFk: '94' }, { itemFk: 4, @@ -43,7 +43,7 @@ describe('entry import()', () => { packing: 1, size: 25, volume: 1125, - packageFk: '94' + packagingFk: '94' } ] } diff --git a/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js b/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js index 41971a64c..c860e228e 100644 --- a/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuysPreview.spec.js @@ -10,12 +10,12 @@ describe('entry importBuysPreview()', () => { }); }); - it('should return the buys with the calculated packageFk', async() => { + it('should return the buys with the calculated packagingFk', async() => { const tx = await models.Entry.beginTransaction({}); const options = {transaction: tx}; try { - const expectedPackageFk = '3'; + const expectedPackagingFk = '3'; const buys = [ { itemFk: 1, @@ -39,7 +39,7 @@ describe('entry importBuysPreview()', () => { const randomIndex = Math.floor(Math.random() * result.length); const buy = result[randomIndex]; - expect(buy.packageFk).toEqual(expectedPackageFk); + expect(buy.packagingFk).toEqual(expectedPackagingFk); await tx.rollback(); } catch (e) { diff --git a/modules/entry/back/models/buy.json b/modules/entry/back/models/buy.json index af205533f..30379eaf6 100644 --- a/modules/entry/back/models/buy.json +++ b/modules/entry/back/models/buy.json @@ -103,7 +103,7 @@ "package": { "type": "belongsTo", "model": "Packaging", - "foreignKey": "packageFk" + "foreignKey": "packagingFk" }, "worker": { "type": "belongsTo", diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html index ccb1a5dac..28396434c 100644 --- a/modules/entry/front/buy/import/index.html +++ b/modules/entry/front/buy/import/index.html @@ -83,14 +83,14 @@ {{::buy.packing | dashIfEmpty}} {{::buy.grouping | dashIfEmpty}} {{::buy.buyingValue | currency: 'EUR':2}} - + + ng-model="buy.packagingFk"> diff --git a/modules/entry/front/buy/index/index.html b/modules/entry/front/buy/index/index.html index 28fdabdb4..203d6c522 100644 --- a/modules/entry/front/buy/index/index.html +++ b/modules/entry/front/buy/index/index.html @@ -88,12 +88,12 @@ diff --git a/modules/entry/front/buy/index/index.js b/modules/entry/front/buy/index/index.js index 649e69286..c991b745b 100644 --- a/modules/entry/front/buy/index/index.js +++ b/modules/entry/front/buy/index/index.js @@ -4,7 +4,7 @@ import Section from 'salix/components/section'; export default class Controller extends Section { saveBuy(buy) { - const missingData = !buy.itemFk || !buy.quantity || !buy.packageFk; + const missingData = !buy.itemFk || !buy.quantity || !buy.packagingFk; if (missingData) return; let options; diff --git a/modules/entry/front/buy/index/index.spec.js b/modules/entry/front/buy/index/index.spec.js index 0e221302c..b9d5fab51 100644 --- a/modules/entry/front/buy/index/index.spec.js +++ b/modules/entry/front/buy/index/index.spec.js @@ -17,7 +17,7 @@ describe('Entry buy', () => { describe('saveBuy()', () => { it(`should call the buys patch route if the received buy has an ID`, () => { - const buy = {id: 1, itemFk: 1, quantity: 1, packageFk: 1}; + const buy = {id: 1, itemFk: 1, quantity: 1, packagingFk: 1}; const query = `Buys/${buy.id}`; diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html index a2cf62da2..16e039cf0 100644 --- a/modules/entry/front/latest-buys/index.html +++ b/modules/entry/front/latest-buys/index.html @@ -104,7 +104,7 @@ Weight - + Package @@ -207,7 +207,7 @@ {{::buy.minPrice | currency: 'EUR':3}} {{::buy.ektFk | dashIfEmpty}} {{::buy.weight}} - {{::buy.packageFk}} + {{::buy.packagingFk}} {{::buy.packingOut}} {{::buy.landing | date: 'dd/MM/yyyy'}} diff --git a/modules/entry/front/latest-buys/index.js b/modules/entry/front/latest-buys/index.js index d9065193e..292c5b805 100644 --- a/modules/entry/front/latest-buys/index.js +++ b/modules/entry/front/latest-buys/index.js @@ -48,7 +48,7 @@ export default class Controller extends Section { } }, { - field: 'packageFk', + field: 'packagingFk', autocomplete: { url: 'Packagings', showField: 'id' @@ -133,7 +133,7 @@ export default class Controller extends Section { case 'price3': case 'ektFk': case 'weight': - case 'packageFk': + case 'packagingFk': return {[`b.${param}`]: value}; } } diff --git a/modules/entry/front/summary/index.html b/modules/entry/front/summary/index.html index d5d3ebf1e..655dcd66f 100644 --- a/modules/entry/front/summary/index.html +++ b/modules/entry/front/summary/index.html @@ -105,7 +105,7 @@ Quantity Stickers - Package + Package Weight Packing Grouping @@ -118,7 +118,7 @@ {{::line.quantity}} {{::line.stickers | dashIfEmpty}} - {{::line.packageFk | dashIfEmpty}} + {{::line.packagingFk | dashIfEmpty}} {{::line.weight}} diff --git a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js index a48664b30..4bba2498f 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js +++ b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js @@ -23,7 +23,7 @@ module.exports = Self => { } }); - Self.makePdfAndNotify = async function(ctx, id, printerFk) { + Self.makePdfAndNotify = async function(ctx, id, printerFk, options) { const models = Self.app.models; options = typeof options == 'object' @@ -59,10 +59,10 @@ module.exports = Self => { }; await Self.invoiceEmail(ctx, ref); } catch (err) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = ctx.req.__('Mail not sent', { clientId: client.id, - clientUrl: `${origin}/#!/claim/${id}/summary` + clientUrl: `${url}claim/${id}/summary` }); const salesPersonId = client.salesPersonFk; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js index 6ecfe6015..22891f161 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/refund.spec.js @@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context'); describe('InvoiceOut refund()', () => { const userId = 5; - const ctx = {req: {accessToken: userId}}; + const ctx = {req: {accessToken: userId}, args: {}}; const withWarehouse = true; const activeCtx = { accessToken: {userId: userId}, diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js new file mode 100644 index 000000000..04f6df299 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js @@ -0,0 +1,68 @@ + +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('InvoiceOut tranferInvoice()', () => { + const activeCtx = { + accessToken: {userId: 5}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + const ctx = {req: activeCtx}; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + + it('should return the id of the created issued invoice', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + const args = { + id: '1', + ref: 'T4444444', + newClientFk: 1, + cplusRectificationId: 1, + cplusInvoiceType477Id: 1, + invoiceCorrectionTypeId: 1 + }; + ctx.args = args; + try { + const result = await models.InvoiceOut.transferInvoice( + ctx, + options); + + expect(result).toBeDefined(); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an UserError when it is the same client', async() => { + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + const args = { + id: '1', + ref: 'T1111111', + newClientFk: 1101, + cplusRectificationId: 1, + cplusInvoiceType477Id: 1, + invoiceCorrectionTypeId: 1 + }; + ctx.args = args; + try { + await models.InvoiceOut.transferInvoice( + ctx, + options); + } catch (e) { + expect(e.message).toBe(`Select a different client`); + await tx.rollback(); + } + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js new file mode 100644 index 000000000..8a0609b8d --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js @@ -0,0 +1,111 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('transferInvoice', { + description: 'Transfer an issued invoice to another client', + accessType: 'WRITE', + accepts: [ + { + arg: 'id', + type: 'number', + required: true, + description: 'Issued invoice id' + }, + { + arg: 'ref', + type: 'string', + required: true + }, + { + arg: 'newClientFk', + type: 'number', + required: true + }, + { + arg: 'cplusRectificationId', + type: 'number', + required: true + }, + { + arg: 'cplusInvoiceType477Id', + type: 'number', + required: true + }, + { + arg: 'invoiceCorrectionTypeId', + type: 'number', + required: true + }, + ], + returns: { + type: 'boolean', + root: true + }, + http: { + path: '/transferInvoice', + verb: 'post' + } + }); + + Self.transferInvoice = async(ctx, options) => { + const models = Self.app.models; + const myOptions = {userId: ctx.req.accessToken.userId}; + const args = ctx.args; + let tx; + if (typeof options == 'object') + Object.assign(myOptions, options); + + const {clientFk} = await models.InvoiceOut.findById(args.id); + + if (clientFk == args.newClientFk) + throw new UserError(`Select a different client`); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + try { + const filterRef = {where: {refFk: args.ref}}; + const tickets = await models.Ticket.find(filterRef, myOptions); + const ticketsIds = tickets.map(ticket => ticket.id); + await models.Ticket.refund(ctx, ticketsIds, null, myOptions); + + const filterTicket = {where: {ticketFk: {inq: ticketsIds}}}; + + const services = await models.TicketService.find(filterTicket, myOptions); + const servicesIds = services.map(service => service.id); + + const sales = await models.Sale.find(filterTicket, myOptions); + const salesIds = sales.map(sale => sale.id); + + const clonedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, false, myOptions); + const clonedTicketIds = []; + + for (const clonedTicket of clonedTickets) { + await clonedTicket.updateAttribute('clientFk', args.newClientFk, myOptions); + clonedTicketIds.push(clonedTicket.id); + } + + const invoiceIds = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, myOptions); + const [invoiceId] = invoiceIds; + + await models.InvoiceCorrection.create({ + correctingFk: invoiceId, + correctedFk: args.id, + cplusRectificationTypeFk: args.cplusRectificationId, + cplusInvoiceType477Fk: args.cplusInvoiceType477Id, + invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId + }, myOptions); + + if (tx) { + await tx.commit(); + await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + } + + return invoiceId; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index 9e8b119ab..23246893b 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -31,5 +31,17 @@ }, "ZipConfig": { "dataSource": "vn" + }, + "CplusRectificationType": { + "dataSource": "vn" + }, + "InvoiceCorrectionType": { + "dataSource": "vn" + }, + "InvoiceCorrection": { + "dataSource": "vn" + }, + "CplusInvoiceType477": { + "dataSource": "vn" } } diff --git a/modules/invoiceOut/back/models/cplus-invoice-type-477.json b/modules/invoiceOut/back/models/cplus-invoice-type-477.json new file mode 100644 index 000000000..840a9a7e4 --- /dev/null +++ b/modules/invoiceOut/back/models/cplus-invoice-type-477.json @@ -0,0 +1,19 @@ +{ + "name": "CplusInvoiceType477", + "base": "VnModel", + "options": { + "mysql": { + "table": "cplusInvoiceType477" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/cplus-rectification-type.json b/modules/invoiceOut/back/models/cplus-rectification-type.json new file mode 100644 index 000000000..e7bfb957f --- /dev/null +++ b/modules/invoiceOut/back/models/cplus-rectification-type.json @@ -0,0 +1,19 @@ +{ + "name": "CplusRectificationType", + "base": "VnModel", + "options": { + "mysql": { + "table": "cplusRectificationType" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-correction-type.json b/modules/invoiceOut/back/models/invoice-correction-type.json new file mode 100644 index 000000000..ad3f034ea --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-correction-type.json @@ -0,0 +1,19 @@ +{ + "name": "InvoiceCorrectionType", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceCorrectionType" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "description": { + "type": "string" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-correction.json b/modules/invoiceOut/back/models/invoice-correction.json new file mode 100644 index 000000000..48bd172a6 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-correction.json @@ -0,0 +1,28 @@ +{ + "name": "InvoiceCorrection", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceCorrection" + } + }, + "properties": { + "correctingFk": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "correctedFk": { + "type": "number" + }, + "cplusRectificationTypeFk": { + "type": "number" + }, + "cplusInvoiceType477Fk": { + "type": "number" + }, + "invoiceCorrectionTypeFk": { + "type": "number" + } + } +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index d3aaf3b3d..ca77c856f 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -23,6 +23,7 @@ module.exports = Self => { require('../methods/invoiceOut/getInvoiceDate')(Self); require('../methods/invoiceOut/negativeBases')(Self); require('../methods/invoiceOut/negativeBasesCsv')(Self); + require('../methods/invoiceOut/transferInvoice')(Self); Self.filePath = async function(id, options) { const fields = ['ref', 'issued']; diff --git a/modules/invoiceOut/front/descriptor-menu/index.html b/modules/invoiceOut/front/descriptor-menu/index.html index 106f8e3cc..7d465f4ea 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.html +++ b/modules/invoiceOut/front/descriptor-menu/index.html @@ -1,3 +1,19 @@ + + + + + + + + Transfer invoice to... + Confirm + + + +
+ + + + #{{id}} - {{::name}} + + + + + {{::description}} + + + + + + + + + +
+
+ + + +
\ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js index 38c3c9434..7d2644158 100644 --- a/modules/invoiceOut/front/descriptor-menu/index.js +++ b/modules/invoiceOut/front/descriptor-menu/index.js @@ -125,6 +125,22 @@ class Controller extends Section { this.$state.go('ticket.card.sale', {id: refundTicket.id}); }); } + + transferInvoice() { + const params = { + id: this.invoiceOut.id, + ref: this.invoiceOut.ref, + newClientFk: this.invoiceOut.client.id, + cplusRectificationId: this.cplusRectificationType, + cplusInvoiceType477Id: this.cplusInvoiceType477, + invoiceCorrectionTypeId: this.invoiceCorrectionType + }; + this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => { + const invoiceId = res.data; + this.vnApp.showSuccess(this.$t('Invoice trasfered!')); + this.$state.go('invoiceOut.card.summary', {id: invoiceId}); + }); + } } Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail']; diff --git a/modules/invoiceOut/front/descriptor-menu/locale/en.yml b/modules/invoiceOut/front/descriptor-menu/locale/en.yml index d299155d7..8fad5f25e 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/en.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/en.yml @@ -1 +1,3 @@ The following refund tickets have been created: "The following refund tickets have been created: {{ticketIds}}" +Transfer invoice to...: Transfer invoice to... +Cplus Type: Cplus Type \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor-menu/locale/es.yml b/modules/invoiceOut/front/descriptor-menu/locale/es.yml index 393efd58c..0f74b5fec 100644 --- a/modules/invoiceOut/front/descriptor-menu/locale/es.yml +++ b/modules/invoiceOut/front/descriptor-menu/locale/es.yml @@ -21,3 +21,5 @@ The invoice PDF document has been regenerated: El documento PDF de la factura ha The email can't be empty: El correo no puede estar vacío The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}" Refund...: Abono... +Transfer invoice to...: Transferir factura a... +Cplus Type: Cplus Tipo diff --git a/modules/invoiceOut/front/descriptor-menu/style.scss b/modules/invoiceOut/front/descriptor-menu/style.scss index b68301961..9e4cf4297 100644 --- a/modules/invoiceOut/front/descriptor-menu/style.scss +++ b/modules/invoiceOut/front/descriptor-menu/style.scss @@ -21,4 +21,10 @@ vn-invoice-out-descriptor-menu { font-size: 1.75rem; } } -} \ No newline at end of file + +} +@media screen and (min-width: 1000px) { + .transferInvoice { + min-width: $width-md; + } +} diff --git a/modules/item/back/methods/item/lastEntriesFilter.js b/modules/item/back/methods/item/lastEntriesFilter.js index 83a8fe7a2..5aafbb4f6 100644 --- a/modules/item/back/methods/item/lastEntriesFilter.js +++ b/modules/item/back/methods/item/lastEntriesFilter.js @@ -29,44 +29,44 @@ module.exports = Self => { Object.assign(myOptions, options); const stmt = new ParameterizedSQL( - `SELECT - w.id AS warehouseFk, - w.name AS warehouse, - tr.landed, - b.id AS buyFk, - b.entryFk, - b.isIgnored, - b.price2, - b.price3, - b.stickers, - b.packing, - b.grouping, - b.groupingMode, - b.weight, - i.stems, - b.quantity, - b.buyingValue + - b.freightValue + - b.comissionValue + - b.packageValue AS cost, - b.buyingValue, - b.freightValue, - b.comissionValue, - b.packageValue, - b.packageFk , - s.id AS supplierFk, - s.name AS supplier - FROM itemType it - RIGHT JOIN (entry e - LEFT JOIN supplier s ON s.id = e.supplierFk - RIGHT JOIN buy b ON b.entryFk = e.id - LEFT JOIN item i ON i.id = b.itemFk - LEFT JOIN ink ON ink.id = i.inkFk - LEFT JOIN travel tr ON tr.id = e.travelFk - LEFT JOIN warehouse w ON w.id = tr.warehouseInFk - LEFT JOIN origin o ON o.id = i.originFk - ) ON it.id = i.typeFk - LEFT JOIN edi.ekt ek ON b.ektFk = ek.id`); + `SELECT w.id AS warehouseFk, + w.name AS warehouse, + tr.landed, + b.id AS buyFk, + b.entryFk, + b.isIgnored, + b.price2, + b.price3, + b.stickers, + b.packing, + b.grouping, + b.groupingMode, + b.weight, + i.stems, + b.quantity, + b.buyingValue + + b.freightValue + + b.comissionValue + + b.packageValue AS cost, + b.buyingValue, + b.freightValue, + b.comissionValue, + b.packageValue, + b.packagingFk , + s.id AS supplierFk, + s.name AS supplier + FROM itemType it + RIGHT JOIN (entry e + LEFT JOIN supplier s ON s.id = e.supplierFk + RIGHT JOIN buy b ON b.entryFk = e.id + LEFT JOIN item i ON i.id = b.itemFk + LEFT JOIN ink ON ink.id = i.inkFk + LEFT JOIN travel tr ON tr.id = e.travelFk + LEFT JOIN warehouse w ON w.id = tr.warehouseInFk + LEFT JOIN origin o ON o.id = i.originFk + ) ON it.id = i.typeFk + LEFT JOIN edi.ekt ek ON b.ektFk = ek.id` + ); stmt.merge(conn.makeSuffix(filter)); return conn.executeStmt(stmt, myOptions); diff --git a/modules/item/back/models/item-shelving.json b/modules/item/back/models/item-shelving.json index 34773e34c..61d05539e 100644 --- a/modules/item/back/models/item-shelving.json +++ b/modules/item/back/models/item-shelving.json @@ -47,6 +47,11 @@ "type": "belongsTo", "model": "VnUser", "foreignKey": "userFk" + }, + "shelving": { + "type": "belongsTo", + "model": "Shelving", + "foreignKey": "shelvingFk" } } } diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json index e99dcd996..6db1f5efc 100644 --- a/modules/item/back/models/item.json +++ b/modules/item/back/models/item.json @@ -131,6 +131,9 @@ "nonRecycledPlastic": { "type": "number" }, + "minQuantity": { + "type": "number" + }, "packingOut": { "type": "number" }, @@ -154,6 +157,10 @@ "mysql":{ "columnName": "doPhoto" } + }, + "minQuantity": { + "type": "number", + "description": "Min quantity" } }, "relations": { diff --git a/modules/item/front/basic-data/index.html b/modules/item/front/basic-data/index.html index fba4d679c..426c17800 100644 --- a/modules/item/front/basic-data/index.html +++ b/modules/item/front/basic-data/index.html @@ -33,6 +33,8 @@ rule info="Full name calculates based on tags 1-3. Is not recommended to change it manually"> + + + + +
{{::name}}
+
+ #{{::id}} +
+
+ + + + +
- - -
{{::name}}
-
- #{{::id}} -
-
- - - - -
+ +
Quantity Cost Kg. - Cube + Cube Provider @@ -94,7 +94,7 @@ {{::entry.weight | dashIfEmpty}} - {{::entry.packageFk | dashIfEmpty}} + {{::entry.packagingFk | dashIfEmpty}} {{::entry.supplier | dashIfEmpty}} diff --git a/modules/item/front/last-entries/index.js b/modules/item/front/last-entries/index.js index 0c6804838..31616f8a7 100644 --- a/modules/item/front/last-entries/index.js +++ b/modules/item/front/last-entries/index.js @@ -71,7 +71,7 @@ class Controller extends Section { switch (param) { case 'id': case 'quantity': - case 'packageFk': + case 'packagingFk': return {[`b.${param}`]: value}; case 'supplierFk': return {[`s.id`]: value}; diff --git a/modules/item/front/request/index.html b/modules/item/front/request/index.html index c505b3a09..03c8db8ec 100644 --- a/modules/item/front/request/index.html +++ b/modules/item/front/request/index.html @@ -26,6 +26,7 @@ Ticket ID Shipped Description + Requester Requested Price Atender @@ -51,6 +52,13 @@ {{::request.description}} + + + {{::request.requesterName}} + + {{::request.quantity}} {{::request.price | currency: 'EUR':2}} @@ -78,8 +86,8 @@ - {{request.itemDescription}} @@ -106,13 +114,13 @@ - - - @@ -141,24 +149,24 @@ ng-click="contextmenu.filterBySelection()"> Filter by selection
- Exclude selection - Remove filter - Remove all filters - Copy value - \ No newline at end of file + diff --git a/modules/item/front/summary/index.html b/modules/item/front/summary/index.html index 6ec738c7e..5fe84591f 100644 --- a/modules/item/front/summary/index.html +++ b/modules/item/front/summary/index.html @@ -128,6 +128,9 @@ + +

diff --git a/modules/item/front/summary/locale/es.yml b/modules/item/front/summary/locale/es.yml index 2e78841ae..80988c491 100644 --- a/modules/item/front/summary/locale/es.yml +++ b/modules/item/front/summary/locale/es.yml @@ -2,3 +2,4 @@ Barcode: Códigos de barras Other data: Otros datos Go to the item: Ir al artículo WarehouseFk: Calculado sobre el almacén de {{ warehouseName }} +Minimum sales quantity: Cantidad mínima de venta diff --git a/modules/order/back/methods/order/catalogFilter.js b/modules/order/back/methods/order/catalogFilter.js index 0d83f9f4a..722f3e096 100644 --- a/modules/order/back/methods/order/catalogFilter.js +++ b/modules/order/back/methods/order/catalogFilter.js @@ -100,31 +100,32 @@ module.exports = Self => { )); stmt = new ParameterizedSQL(` - SELECT - i.id, - i.name, - i.subName, - i.image, - i.tag5, - i.value5, - i.tag6, - i.value6, - i.tag7, - i.value7, - i.tag8, - i.value8, - i.stars, - tci.price, - tci.available, - w.lastName AS lastName, - w.firstName, - tci.priceKg, - ink.hex + SELECT i.id, + i.name, + i.subName, + i.image, + i.tag5, + i.value5, + i.tag6, + i.value6, + i.tag7, + i.value7, + i.tag8, + i.value8, + i.stars, + tci.price, + tci.available, + w.lastName, + w.firstName, + tci.priceKg, + ink.hex, + i.minQuantity FROM tmp.ticketCalculateItem tci JOIN vn.item i ON i.id = tci.itemFk JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.worker w on w.id = it.workerFk - LEFT JOIN vn.ink ON ink.id = i.inkFk`); + LEFT JOIN vn.ink ON ink.id = i.inkFk + `); // Apply order by tag if (orderBy.isTag) { diff --git a/modules/order/front/catalog-view/index.html b/modules/order/front/catalog-view/index.html index fca728855..5d60211ed 100644 --- a/modules/order/front/catalog-view/index.html +++ b/modules/order/front/catalog-view/index.html @@ -8,12 +8,12 @@
- @@ -37,13 +37,28 @@ value="{{::item.value7}}">
- - + + + + + + + + + + {{::item.minQuantity}} + + @@ -69,4 +84,4 @@ - \ No newline at end of file + diff --git a/modules/order/front/catalog-view/locale/es.yml b/modules/order/front/catalog-view/locale/es.yml index 82fe5e9e8..8fb3c7896 100644 --- a/modules/order/front/catalog-view/locale/es.yml +++ b/modules/order/front/catalog-view/locale/es.yml @@ -1 +1,2 @@ Order created: Orden creada +Minimal quantity: Cantidad mínima \ No newline at end of file diff --git a/modules/order/front/catalog-view/style.scss b/modules/order/front/catalog-view/style.scss index 87f70cde5..1e48745ca 100644 --- a/modules/order/front/catalog-view/style.scss +++ b/modules/order/front/catalog-view/style.scss @@ -44,4 +44,7 @@ vn-order-catalog { height: 30px; position: relative; } -} \ No newline at end of file + .alert { + color: $color-alert; + } +} diff --git a/modules/order/front/summary/index.html b/modules/order/front/summary/index.html index 3622ae932..218359992 100644 --- a/modules/order/front/summary/index.html +++ b/modules/order/front/summary/index.html @@ -48,8 +48,10 @@ - + + diff --git a/modules/route/front/roadmap/summary/index.html b/modules/route/front/roadmap/summary/index.html index e6b50601e..9fab0bf87 100644 --- a/modules/route/front/roadmap/summary/index.html +++ b/modules/route/front/roadmap/summary/index.html @@ -25,7 +25,10 @@ + > + { - Self.remoteMethod('addExpeditionState', { + Self.remoteMethodCtx('addExpeditionState', { description: 'Update an expedition state', accessType: 'WRITE', accepts: [ @@ -12,18 +12,15 @@ module.exports = Self => { description: 'Array of objects containing expeditionFk and stateCode' } ], - returns: { - type: 'boolean', - root: true - }, http: { path: `/addExpeditionState`, verb: 'post' } }); - Self.addExpeditionState = async(expeditions, options) => { + Self.addExpeditionState = async(ctx, expeditions, options) => { const models = Self.app.models; + const userId = ctx.req.accessToken.userId; let tx; const myOptions = {}; if (typeof options == 'object') @@ -51,6 +48,7 @@ module.exports = Self => { await models.ExpeditionState.create({ expeditionFk: expedition.expeditionFk, typeFk, + userFk: userId, }, myOptions); } diff --git a/modules/ticket/back/methods/expedition-state/specs/addExpeditionState.spec.js b/modules/ticket/back/methods/expedition-state/specs/addExpeditionState.spec.js index 819a43a60..6c7739006 100644 --- a/modules/ticket/back/methods/expedition-state/specs/addExpeditionState.spec.js +++ b/modules/ticket/back/methods/expedition-state/specs/addExpeditionState.spec.js @@ -1,9 +1,9 @@ const models = require('vn-loopback/server/server').models; describe('expeditionState addExpeditionState()', () => { + const ctx = {req: {accessToken: {userId: 9}}}; it('should update the expedition states', async() => { const tx = await models.ExpeditionState.beginTransaction({}); - try { const options = {transaction: tx}; const payload = [ @@ -13,7 +13,7 @@ describe('expeditionState addExpeditionState()', () => { }, ]; - await models.ExpeditionState.addExpeditionState(payload, options); + await models.ExpeditionState.addExpeditionState(ctx, payload, options); const expeditionState = await models.ExpeditionState.findOne({ where: {id: 5} @@ -39,7 +39,7 @@ describe('expeditionState addExpeditionState()', () => { stateCode: 'DUMMY' } ]; - await models.ExpeditionState.addExpeditionState(payload, options); + await models.ExpeditionState.addExpeditionState(ctx, payload, options); await tx.rollback(); } catch (e) { @@ -62,7 +62,7 @@ describe('expeditionState addExpeditionState()', () => { } ]; - await models.ExpeditionState.addExpeditionState(payload, options); + await models.ExpeditionState.addExpeditionState(ctx, payload, options); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/methods/sale/clone.js b/modules/ticket/back/methods/sale/clone.js new file mode 100644 index 000000000..a5ccb6de4 --- /dev/null +++ b/modules/ticket/back/methods/sale/clone.js @@ -0,0 +1,122 @@ +module.exports = Self => { + Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, group, negative, options) => { + const models = Self.app.models; + const myOptions = {}; + let tx; + const newTickets = []; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const salesFilter = { + where: {id: {inq: salesIds}}, + include: { + relation: 'components', + scope: { + fields: ['saleFk', 'componentFk', 'value'] + } + } + }; + const sales = await models.Sale.find(salesFilter, myOptions); + let ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; + + const mappedTickets = new Map(); + + if (group) ticketsIds = [ticketsIds[0]]; + + for (let ticketId of ticketsIds) { + const newTicket = await createTicket( + ctx, + ticketId, + withWarehouse, + negative, + myOptions + ); + newTickets.push(newTicket); + mappedTickets.set(ticketId, newTicket.id); + } + + for (const sale of sales) { + const newTicketId = mappedTickets.get(sale.ticketFk); + + const createdSale = await models.Sale.create({ + ticketFk: newTicketId, + itemFk: sale.itemFk, + quantity: negative ? - sale.quantity : sale.quantity, + concept: sale.concept, + price: sale.price, + discount: sale.discount, + }, myOptions); + + const components = sale.components(); + for (const component of components) + component.saleFk = createdSale.id; + + await models.SaleComponent.create(components, myOptions); + } + + if (servicesIds && servicesIds.length) { + const servicesFilter = { + where: {id: {inq: servicesIds}} + }; + const services = await models.TicketService.find(servicesFilter, myOptions); + + for (const service of services) { + const newTicketId = mappedTickets.get(service.ticketFk); + + await models.TicketService.create({ + description: service.description, + quantity: negative ? - service.quantity : service.quantity, + price: service.price, + taxClassFk: service.taxClassFk, + ticketFk: newTicketId, + ticketServiceTypeFk: service.ticketServiceTypeFk, + }, myOptions); + } + } + + if (tx) await tx.commit(); + + return newTickets; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + + async function createTicket( + ctx, + ticketId, + withWarehouse, + negative, + myOptions + ) { + const models = Self.app.models; + const now = Date.vnNew(); + + const ticket = await models.Ticket.findById(ticketId, null, myOptions); + ctx.args.clientId = ticket.clientFk; + ctx.args.shipped = now; + ctx.args.landed = now; + ctx.args.warehouseId = withWarehouse ? ticket.warehouseFk : null; + ctx.args.companyId = ticket.companyFk; + ctx.args.addressId = ticket.addressFk; + + const newTicket = await models.Ticket.new(ctx, myOptions); + + if (negative) { + await models.TicketRefund.create({ + originalTicketFk: ticketId, + refundTicketFk: newTicket.id + }, myOptions); + } + + return newTicket; + } + }; +}; diff --git a/modules/ticket/back/methods/sale/deleteSales.js b/modules/ticket/back/methods/sale/deleteSales.js index 5d1463a66..0207815a9 100644 --- a/modules/ticket/back/methods/sale/deleteSales.js +++ b/modules/ticket/back/methods/sale/deleteSales.js @@ -1,5 +1,3 @@ -let UserError = require('vn-loopback/util/user-error'); - module.exports = Self => { Self.remoteMethodCtx('deleteSales', { description: 'Deletes the selected sales', @@ -70,11 +68,11 @@ module.exports = Self => { const salesPerson = ticket.client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Deleted sales from ticket', { ticketId: ticketId, - ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, + ticketUrl: `${url}ticket/${ticketId}/sale`, deletions: deletions }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); diff --git a/modules/ticket/back/methods/sale/refund.js b/modules/ticket/back/methods/sale/refund.js index 3c41aab1e..17b70f12b 100644 --- a/modules/ticket/back/methods/sale/refund.js +++ b/modules/ticket/back/methods/sale/refund.js @@ -5,7 +5,8 @@ module.exports = Self => { accepts: [ { arg: 'salesIds', - type: ['number'] + type: ['number'], + required: true }, { arg: 'servicesIds', @@ -40,124 +41,23 @@ module.exports = Self => { myOptions.transaction = tx; } - let refundTicket = null; try { - const refundAgencyMode = await models.AgencyMode.findOne({ - include: { - relation: 'zones', - scope: { - limit: 1, - field: ['id', 'name'] - } - }, - where: {code: 'refund'} - }, myOptions); - - const refoundZoneId = refundAgencyMode.zones()[0].id; - - if (salesIds) { - const salesFilter = { - where: {id: {inq: salesIds}}, - include: { - relation: 'components', - scope: { - fields: ['saleFk', 'componentFk', 'value'] - } - } - }; - const sales = await models.Sale.find(salesFilter, myOptions); - const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))]; - - const now = Date.vnNew(); - const [firstTicketId] = ticketsIds; - - // eslint-disable-next-line max-len - refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); - - for (const sale of sales) { - const createdSale = await models.Sale.create({ - ticketFk: refundTicket.id, - itemFk: sale.itemFk, - quantity: - sale.quantity, - concept: sale.concept, - price: sale.price, - discount: sale.discount, - }, myOptions); - - const components = sale.components(); - for (const component of components) - component.saleFk = createdSale.id; - - await models.SaleComponent.create(components, myOptions); - } - } - - if (!refundTicket) { - const servicesFilter = { - where: {id: {inq: servicesIds}} - }; - const services = await models.TicketService.find(servicesFilter, myOptions); - const ticketsIds = [...new Set(services.map(service => service.ticketFk))]; - - const now = Date.vnNew(); - const [firstTicketId] = ticketsIds; - - // eslint-disable-next-line max-len - refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); - } - - if (servicesIds && servicesIds.length > 0) { - const servicesFilter = { - where: {id: {inq: servicesIds}} - }; - const services = await models.TicketService.find(servicesFilter, myOptions); - for (const service of services) { - await models.TicketService.create({ - description: service.description, - quantity: service.quantity, - price: - service.price, - taxClassFk: service.taxClassFk, - ticketFk: refundTicket.id, - ticketServiceTypeFk: service.ticketServiceTypeFk, - }, myOptions); - } - } - - const query = `CALL vn.ticket_recalc(?, NULL)`; - await Self.rawSql(query, [refundTicket.id], myOptions); + const refundsTicket = await models.Sale.clone( + ctx, + salesIds, + servicesIds, + withWarehouse, + false, + true, + myOptions + ); if (tx) await tx.commit(); - return refundTicket; + return refundsTicket[0]; } catch (e) { if (tx) await tx.rollback(); throw e; } }; - - async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions) { - const models = Self.app.models; - - const filter = {include: {relation: 'address'}}; - const ticket = await models.Ticket.findById(ticketId, filter, myOptions); - - const refundTicket = await models.Ticket.create({ - clientFk: ticket.clientFk, - shipped: now, - addressFk: ticket.address().id, - agencyModeFk: refundAgencyMode.id, - nickname: ticket.address().nickname, - warehouseFk: withWarehouse ? ticket.warehouseFk : null, - companyFk: ticket.companyFk, - landed: now, - zoneFk: refoundZoneId - }, myOptions); - - await models.TicketRefund.create({ - refundTicketFk: refundTicket.id, - originalTicketFk: ticket.id, - }, myOptions); - - return refundTicket; - } }; diff --git a/modules/ticket/back/methods/sale/reserve.js b/modules/ticket/back/methods/sale/reserve.js index 2dc368af6..36db791fc 100644 --- a/modules/ticket/back/methods/sale/reserve.js +++ b/modules/ticket/back/methods/sale/reserve.js @@ -1,6 +1,3 @@ - -let UserError = require('vn-loopback/util/user-error'); - module.exports = Self => { Self.remoteMethodCtx('reserve', { description: 'Change the state of a ticket', @@ -65,7 +62,8 @@ module.exports = Self => { promises.push(reservedSale); - changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`; + changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) + ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`; } } @@ -87,11 +85,11 @@ module.exports = Self => { const salesPerson = ticket.client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Changed sale reserved state', { ticketId: ticketId, - ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, + ticketUrl: `${url}ticket/${ticketId}/sale`, changes: changesMade }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); diff --git a/modules/ticket/back/methods/sale/specs/refund.spec.js b/modules/ticket/back/methods/sale/specs/refund.spec.js index b81f7f84d..08eb1fabd 100644 --- a/modules/ticket/back/methods/sale/specs/refund.spec.js +++ b/modules/ticket/back/methods/sale/specs/refund.spec.js @@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context'); describe('Sale refund()', () => { const userId = 5; - const ctx = {req: {accessToken: userId}}; + const ctx = {req: {accessToken: userId}, args: {}}; const activeCtx = { accessToken: {userId}, }; @@ -40,6 +40,7 @@ describe('Sale refund()', () => { try { const options = {transaction: tx}; + const ticketsBefore = await models.Ticket.find({}, options); const ticket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options); @@ -61,12 +62,13 @@ describe('Sale refund()', () => { } ] }, options); - + const ticketsAfter = await models.Ticket.find({}, options); const salesLength = refundedTicket.ticketSales().length; const componentsLength = refundedTicket.ticketSales()[0].components().length; expect(refundedTicket).toBeDefined(); - expect(salesLength).toEqual(2); + expect(salesLength).toEqual(1); + expect(ticketsBefore.length).toEqual(ticketsAfter.length - 2); expect(componentsLength).toEqual(4); await tx.rollback(); diff --git a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js deleted file mode 100644 index 8064ea30b..000000000 --- a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js +++ /dev/null @@ -1,162 +0,0 @@ -const models = require('vn-loopback/server/server').models; -const LoopBackContext = require('loopback-context'); - -describe('sale updateQuantity()', () => { - beforeAll(async() => { - const activeCtx = { - accessToken: {userId: 9}, - http: { - req: { - headers: {origin: 'http://localhost'} - } - } - }; - spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ - active: activeCtx - }); - }); - - const ctx = { - req: { - accessToken: {userId: 9}, - headers: {origin: 'localhost:5000'}, - __: () => {} - } - }; - - it('should throw an error if the quantity is greater than it should be', async() => { - const ctx = { - req: { - accessToken: {userId: 1}, - headers: {origin: 'localhost:5000'}, - __: () => {} - } - }; - const tx = await models.Sale.beginTransaction({}); - - let error; - try { - const options = {transaction: tx}; - - await models.Sale.updateQuantity(ctx, 17, 99, options); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - error = e; - } - - expect(error).toEqual(new Error('The new quantity should be smaller than the old one')); - }); - - it('should add quantity if the quantity is greater than it should be and is role advanced', async() => { - const tx = await models.Sale.beginTransaction({}); - const saleId = 17; - const buyerId = 35; - const ctx = { - req: { - accessToken: {userId: buyerId}, - headers: {origin: 'localhost:5000'}, - __: () => {} - } - }; - - try { - const options = {transaction: tx}; - - const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*'); - - expect(isRoleAdvanced).toEqual(true); - - const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - - expect(originalLine.quantity).toEqual(30); - - const newQuantity = originalLine.quantity + 1; - await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); - - const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - - expect(modifiedLine.quantity).toEqual(newQuantity); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should update the quantity of a given sale current line', async() => { - const tx = await models.Sale.beginTransaction({}); - const saleId = 25; - const newQuantity = 4; - - try { - const options = {transaction: tx}; - - const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - - expect(originalLine.quantity).toEqual(20); - - await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); - - const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - - expect(modifiedLine.quantity).toEqual(newQuantity); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); - - it('should throw an error if the quantity is negative and it is not a refund ticket', async() => { - const ctx = { - req: { - accessToken: {userId: 1}, - headers: {origin: 'localhost:5000'}, - __: () => {} - } - }; - const saleId = 17; - const newQuantity = -10; - - const tx = await models.Sale.beginTransaction({}); - - let error; - try { - const options = {transaction: tx}; - - await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - error = e; - } - - expect(error).toEqual(new Error('You can only add negative amounts in refund tickets')); - }); - - it('should update a negative quantity when is a ticket refund', async() => { - const tx = await models.Sale.beginTransaction({}); - const saleId = 13; - const newQuantity = -10; - - try { - const options = {transaction: tx}; - - await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); - - const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - - expect(modifiedLine.quantity).toEqual(newQuantity); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); diff --git a/modules/ticket/back/methods/sale/updatePrice.js b/modules/ticket/back/methods/sale/updatePrice.js index 62e4ebd42..191fd09e3 100644 --- a/modules/ticket/back/methods/sale/updatePrice.js +++ b/modules/ticket/back/methods/sale/updatePrice.js @@ -1,5 +1,3 @@ -let UserError = require('vn-loopback/util/user-error'); - module.exports = Self => { Self.remoteMethodCtx('updatePrice', { description: 'Changes the price of a sale', @@ -100,7 +98,7 @@ module.exports = Self => { const salesPerson = sale.ticket().client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Changed sale price', { ticketId: sale.ticket().id, itemId: sale.itemFk, @@ -108,8 +106,8 @@ module.exports = Self => { quantity: sale.quantity, oldPrice: oldPrice, newPrice: newPrice, - ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, - itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + ticketUrl: `${url}ticket/${sale.ticket().id}/sale`, + itemUrl: `${url}item/${sale.itemFk}/summary` }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); } diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js index edbc34e42..1a497da24 100644 --- a/modules/ticket/back/methods/sale/updateQuantity.js +++ b/modules/ticket/back/methods/sale/updateQuantity.js @@ -1,4 +1,3 @@ -let UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethodCtx('updateQuantity', { @@ -64,32 +63,22 @@ module.exports = Self => { const sale = await models.Sale.findById(id, filter, myOptions); - const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*'); - if (newQuantity > sale.quantity && !isRoleAdvanced) - throw new UserError('The new quantity should be smaller than the old one'); - - const ticketRefund = await models.TicketRefund.findOne({ - where: {refundTicketFk: sale.ticketFk}, - fields: ['id']} - , myOptions); - if (newQuantity < 0 && !ticketRefund) - throw new UserError('You can only add negative amounts in refund tickets'); - const oldQuantity = sale.quantity; const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); const salesPerson = sale.ticket().client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Changed sale quantity', { ticketId: sale.ticket().id, itemId: sale.itemFk, concept: sale.concept, oldQuantity: oldQuantity, newQuantity: newQuantity, - ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, - itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + ticketUrl: `${url}ticket/${sale.ticket().id}/sale`, + itemUrl: `${url}item/${sale.itemFk}/summary` }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); } diff --git a/modules/ticket/back/methods/ticket-request/confirm.js b/modules/ticket/back/methods/ticket-request/confirm.js index 9cd7f4d68..00310f33c 100644 --- a/modules/ticket/back/methods/ticket-request/confirm.js +++ b/modules/ticket/back/methods/ticket-request/confirm.js @@ -84,7 +84,7 @@ module.exports = Self => { const query = `CALL vn.sale_calculateComponent(?, NULL)`; await Self.rawSql(query, [sale.id], myOptions); - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const requesterId = request.requesterFk; const message = $t('Bought units from buy request', { @@ -92,8 +92,8 @@ module.exports = Self => { concept: sale.concept, itemId: sale.itemFk, ticketId: sale.ticketFk, - url: `${origin}/#!/ticket/${sale.ticketFk}/summary`, - urlItem: `${origin}/#!/item/${sale.itemFk}/summary` + url: `${url}ticket/${sale.ticketFk}/summary`, + urlItem: `${url}item/${sale.itemFk}/summary` }); await models.Chat.sendCheckingPresence(ctx, requesterId, message, myOptions); diff --git a/modules/ticket/back/methods/ticket-request/deny.js b/modules/ticket/back/methods/ticket-request/deny.js index 92f020083..44f1e48a1 100644 --- a/modules/ticket/back/methods/ticket-request/deny.js +++ b/modules/ticket/back/methods/ticket-request/deny.js @@ -50,12 +50,12 @@ module.exports = Self => { const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions); await request.updateAttributes(params, myOptions); - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const requesterId = request.requesterFk; const message = $t('Deny buy request', { ticketId: request.ticketFk, - url: `${origin}/#!/ticket/${request.ticketFk}/request/index`, + url: `${url}ticket/${request.ticketFk}/request/index`, observation: params.response }); diff --git a/modules/ticket/back/methods/ticket-request/filter.js b/modules/ticket/back/methods/ticket-request/filter.js index f27ea5018..10aaf02e5 100644 --- a/modules/ticket/back/methods/ticket-request/filter.js +++ b/modules/ticket/back/methods/ticket-request/filter.js @@ -99,6 +99,8 @@ module.exports = Self => { switch (value) { case 'pending': return {'tr.isOk': null}; + case 'accepted': + return {'tr.isOk': 1}; default: return {'tr.isOk': value}; } @@ -122,8 +124,7 @@ module.exports = Self => { filter = mergeFilters(filter, {where}); const stmt = new ParameterizedSQL( - `SELECT - tr.id, + `SELECT tr.id, tr.ticketFk, tr.quantity, tr.price, @@ -133,18 +134,19 @@ module.exports = Self => { tr.saleFk, tr.requesterFk, tr.isOk, - s.quantity AS saleQuantity, + s.quantity saleQuantity, s.itemFk, - i.name AS itemDescription, + i.name itemDescription, t.shipped, - DATE(t.shipped) AS shippedDate, + DATE(t.shipped) shippedDate, t.nickname, t.warehouseFk, t.clientFk, - w.name AS warehouse, - u.nickname AS salesPersonNickname, - ua.name AS attenderName, - c.salesPersonFk + w.name warehouse, + u.nickname salesPersonNickname, + ua.name attenderName, + c.salesPersonFk, + ua2.name requesterName FROM ticketRequest tr LEFT JOIN ticketWeekly tw on tw.ticketFk = tr.ticketFk LEFT JOIN ticket t ON t.id = tr.ticketFk @@ -155,7 +157,8 @@ module.exports = Self => { LEFT JOIN worker wk ON wk.id = c.salesPersonFk LEFT JOIN account.user u ON u.id = wk.id LEFT JOIN worker wka ON wka.id = tr.attenderFk - LEFT JOIN account.user ua ON ua.id = wka.id`); + LEFT JOIN account.user ua ON ua.id = wka.id + LEFT JOIN account.user ua2 ON ua2.id = tr.requesterFk`); stmt.merge(conn.makeSuffix(filter)); return conn.executeStmt(stmt, myOptions); diff --git a/modules/ticket/back/methods/ticket-tracking/setDelivered.js b/modules/ticket/back/methods/ticket-tracking/setDelivered.js index df482fd01..d3cdb192f 100644 --- a/modules/ticket/back/methods/ticket-tracking/setDelivered.js +++ b/modules/ticket/back/methods/ticket-tracking/setDelivered.js @@ -47,7 +47,7 @@ module.exports = Self => { const promises = []; for (const id of ticketIds) { - const promise = models.TicketTracking.changeState(ctx, { + const promise = await models.Ticket.state(ctx, { stateFk: state.id, workerFk: worker.id, ticketFk: id diff --git a/modules/ticket/back/methods/ticket/addSale.js b/modules/ticket/back/methods/ticket/addSale.js index cbf884273..826de6e12 100644 --- a/modules/ticket/back/methods/ticket/addSale.js +++ b/modules/ticket/back/methods/ticket/addSale.js @@ -1,5 +1,3 @@ -const UserError = require('vn-loopback/util/user-error'); - module.exports = Self => { Self.remoteMethodCtx('addSale', { description: 'Inserts a new sale for the current ticket', @@ -63,17 +61,6 @@ module.exports = Self => { } }, myOptions); - const itemInfo = await models.Item.getVisibleAvailable( - itemId, - ticket.warehouseFk, - ticket.shipped, - myOptions - ); - - const isPackaging = item.family == 'EMB'; - if (!isPackaging && itemInfo.available < quantity) - throw new UserError(`This item is not available`); - const newSale = await models.Sale.create({ ticketFk: id, itemFk: item.id, @@ -94,11 +81,11 @@ module.exports = Self => { const salesPerson = ticket.client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t('Added sale to ticket', { ticketId: id, - ticketUrl: `${origin}/#!/ticket/${id}/sale`, + ticketUrl: `${url}ticket/${id}/sale`, addition: addition }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index 8aad8959b..f7c36f108 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -237,7 +237,7 @@ module.exports = Self => { const salesPersonId = originalTicket.client().salesPersonFk; if (salesPersonId) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); let changesMade = ''; for (let change in newProperties) { @@ -249,7 +249,7 @@ module.exports = Self => { const message = $t('Changed this data from the ticket', { ticketId: args.id, - ticketUrl: `${origin}/#!/ticket/${args.id}/sale`, + ticketUrl: `${url}ticket/${args.id}/sale`, changes: changesMade }); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); diff --git a/modules/ticket/back/methods/ticket/getTicketsAdvance.js b/modules/ticket/back/methods/ticket/getTicketsAdvance.js index ec9314db2..ab40b9559 100644 --- a/modules/ticket/back/methods/ticket/getTicketsAdvance.js +++ b/modules/ticket/back/methods/ticket/getTicketsAdvance.js @@ -28,32 +28,27 @@ module.exports = Self => { { arg: 'ipt', type: 'string', - description: 'Origin Item Packaging Type', - required: false + description: 'Origin Item Packaging Type' }, { arg: 'futureIpt', type: 'string', - description: 'Destination Item Packaging Type', - required: false + description: 'Destination Item Packaging Type' }, { arg: 'id', type: 'number', - description: 'Origin id', - required: false + description: 'Origin id' }, { arg: 'futureId', type: 'number', - description: 'Destination id', - required: false + description: 'Destination id' }, { arg: 'isFullMovable', type: 'boolean', - description: 'True when lines and stock of origin are equal', - required: false + description: 'True when lines and stock of origin are equal' }, { arg: 'filter', diff --git a/modules/ticket/back/methods/ticket/invoiceTickets.js b/modules/ticket/back/methods/ticket/invoiceTickets.js index ca1bf15fb..fa3ee93af 100644 --- a/modules/ticket/back/methods/ticket/invoiceTickets.js +++ b/modules/ticket/back/methods/ticket/invoiceTickets.js @@ -77,9 +77,10 @@ module.exports = function(Self) { if (tx) await tx.rollback(); throw e; } - - for (const invoiceId of invoicesIds) - await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + if (tx) { + for (const invoiceId of invoicesIds) + await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null); + } return invoicesIds; }; diff --git a/modules/ticket/back/methods/ticket/merge.js b/modules/ticket/back/methods/ticket/merge.js index 3c0da077f..1106cef06 100644 --- a/modules/ticket/back/methods/ticket/merge.js +++ b/modules/ticket/back/methods/ticket/merge.js @@ -25,7 +25,7 @@ module.exports = Self => { Self.merge = async(ctx, tickets, options) => { const httpRequest = ctx.req; const $t = httpRequest.__; - const origin = httpRequest.headers.origin; + const url = await Self.app.models.Url.getUrl(); const models = Self.app.models; const myOptions = {}; let tx; @@ -40,8 +40,8 @@ module.exports = Self => { try { for (let ticket of tickets) { - const originFullPath = `${origin}/#!/ticket/${ticket.originId}/summary`; - const destinationFullPath = `${origin}/#!/ticket/${ticket.destinationId}/summary`; + const originFullPath = `${url}ticket/${ticket.originId}/summary`; + const destinationFullPath = `${url}ticket/${ticket.destinationId}/summary`; const message = $t('Ticket merged', { originDated: dateUtil.toString(new Date(ticket.originShipped)), destinationDated: dateUtil.toString(new Date(ticket.destinationShipped)), diff --git a/modules/ticket/back/methods/ticket/new.js b/modules/ticket/back/methods/ticket/new.js index b461fb26d..288d38d77 100644 --- a/modules/ticket/back/methods/ticket/new.js +++ b/modules/ticket/back/methods/ticket/new.js @@ -48,7 +48,7 @@ module.exports = Self => { description: `The route id filter` }], returns: { - type: 'number', + type: 'object', root: true }, http: { @@ -96,7 +96,7 @@ module.exports = Self => { if (address.client().type().code === 'normal' && (!agencyMode || agencyMode.code != 'refund')) { const canCreateTicket = await models.Client.canCreateTicket(args.clientId, myOptions); if (!canCreateTicket) - throw new UserError(`You can't create a ticket for a inactive client`); + throw new UserError(`You can't create a ticket for an inactive client`); } if (!args.shipped && args.landed) { diff --git a/modules/ticket/back/methods/ticket/refund.js b/modules/ticket/back/methods/ticket/refund.js index c99b6aa83..758384ae2 100644 --- a/modules/ticket/back/methods/ticket/refund.js +++ b/modules/ticket/back/methods/ticket/refund.js @@ -39,7 +39,6 @@ module.exports = Self => { try { const filter = {where: {ticketFk: {inq: ticketsIds}}}; - const sales = await models.Sale.find(filter, myOptions); const salesIds = sales.map(sale => sale.id); diff --git a/modules/ticket/back/methods/ticket/restore.js b/modules/ticket/back/methods/ticket/restore.js index 722c3294e..e268c3891 100644 --- a/modules/ticket/back/methods/ticket/restore.js +++ b/modules/ticket/back/methods/ticket/restore.js @@ -48,10 +48,10 @@ module.exports = Self => { // Send notification to salesPerson const salesPersonId = ticket.client().salesPersonFk; if (salesPersonId) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t(`I have restored the ticket id`, { id: id, - url: `${origin}/#!/ticket/${id}/summary` + url: `${url}ticket/${id}/summary` }); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); } diff --git a/modules/ticket/back/methods/ticket/setDeleted.js b/modules/ticket/back/methods/ticket/setDeleted.js index fcd9972fe..9a9fd9056 100644 --- a/modules/ticket/back/methods/ticket/setDeleted.js +++ b/modules/ticket/back/methods/ticket/setDeleted.js @@ -119,10 +119,10 @@ module.exports = Self => { // Send notification to salesPerson const salesPersonUser = ticket.client().salesPersonUser(); if (salesPersonUser && sales.length) { - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const message = $t(`I have deleted the ticket id`, { id: id, - url: `${origin}/#!/ticket/${id}/summary` + url: `${url}ticket/${id}/summary` }); await models.Chat.send(ctx, `@${salesPersonUser.name}`, message); } @@ -146,7 +146,8 @@ module.exports = Self => { JOIN vn.sectorCollection sc ON sc.id = scsg.sectorCollectionFk JOIN vn.saleGroupDetail sgd ON sgd.saleGroupFk = sg.id JOIN vn.sale s ON s.id = sgd.saleFk - WHERE s.ticketFk = ?;`, [ticket.id], myOptions); + WHERE s.ticketFk = ?;`, [ticket.id], myOptions + ); if (tx) await tx.commit(); diff --git a/modules/ticket/back/methods/ticket/specs/new.spec.js b/modules/ticket/back/methods/ticket/specs/new.spec.js index 0a2f93bc4..9aa073a7b 100644 --- a/modules/ticket/back/methods/ticket/specs/new.spec.js +++ b/modules/ticket/back/methods/ticket/specs/new.spec.js @@ -30,7 +30,7 @@ describe('ticket new()', () => { await tx.rollback(); } - expect(error).toEqual(new UserError(`You can't create a ticket for a inactive client`)); + expect(error).toEqual(new UserError(`You can't create a ticket for an inactive client`)); }); it('should throw an error if the address doesnt exist', async() => { diff --git a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js b/modules/ticket/back/methods/ticket/specs/state.spec.js similarity index 87% rename from modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js rename to modules/ticket/back/methods/ticket/specs/state.spec.js index 175bc4e4b..9b5e80165 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js +++ b/modules/ticket/back/methods/ticket/specs/state.spec.js @@ -1,7 +1,7 @@ const models = require('vn-loopback/server/server').models; const LoopBackContext = require('loopback-context'); -describe('ticket changeState()', () => { +describe('ticket state()', () => { const salesPersonId = 18; const employeeId = 1; const productionId = 49; @@ -47,7 +47,7 @@ describe('ticket changeState()', () => { activeCtx.accessToken.userId = salesPersonId; const params = {ticketFk: 2, stateFk: 3}; - await models.TicketTracking.changeState(ctx, params, options); + await models.Ticket.state(ctx, params, options); await tx.rollback(); } catch (e) { @@ -69,7 +69,7 @@ describe('ticket changeState()', () => { activeCtx.accessToken.userId = employeeId; const params = {ticketFk: 11, stateFk: 13}; - await models.TicketTracking.changeState(ctx, params, options); + await models.Ticket.state(ctx, params, options); await tx.rollback(); } catch (e) { @@ -80,7 +80,8 @@ describe('ticket changeState()', () => { expect(error.code).toBe('ACCESS_DENIED'); }); - it('should be able to create a ticket tracking line for a not editable ticket if the user has the production role', async() => { + it('should be able to create a ticket tracking line for a not' + + ' editable ticket if the user has the production role', async() => { const tx = await models.TicketTracking.beginTransaction({}); try { @@ -91,7 +92,7 @@ describe('ticket changeState()', () => { activeCtx.accessToken.userId = productionId; const params = {ticketFk: ticket.id, stateFk: 3}; - const ticketTracking = await models.TicketTracking.changeState(ctx, params, options); + const ticketTracking = await models.Ticket.state(ctx, params, options); expect(ticketTracking.__data.ticketFk).toBe(params.ticketFk); expect(ticketTracking.__data.stateFk).toBe(params.stateFk); @@ -105,7 +106,8 @@ describe('ticket changeState()', () => { } }); - it('should update the ticket tracking line when the user is salesperson, uses the state assigned and a valid worker id', async() => { + it('should update the ticket tracking line when the user is salesperson,' + + ' uses the state assigned and a valid worker id', async() => { const tx = await models.TicketTracking.beginTransaction({}); try { @@ -115,7 +117,7 @@ describe('ticket changeState()', () => { const ctx = {req: {accessToken: {userId: 18}}}; const assignedState = await models.State.findOne({where: {code: 'PICKER_DESIGNED'}}, options); const params = {ticketFk: ticket.id, stateFk: assignedState.id, workerFk: 1}; - const res = await models.TicketTracking.changeState(ctx, params, options); + const res = await models.Ticket.state(ctx, params, options); expect(res.__data.ticketFk).toBe(params.ticketFk); expect(res.__data.stateFk).toBe(params.stateFk); diff --git a/modules/ticket/back/methods/ticket-tracking/changeState.js b/modules/ticket/back/methods/ticket/state.js similarity index 94% rename from modules/ticket/back/methods/ticket-tracking/changeState.js rename to modules/ticket/back/methods/ticket/state.js index dbef8762e..01bfbba20 100644 --- a/modules/ticket/back/methods/ticket-tracking/changeState.js +++ b/modules/ticket/back/methods/ticket/state.js @@ -1,7 +1,7 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { - Self.remoteMethodCtx('changeState', { + Self.remoteMethodCtx('state', { description: 'Change the state of a ticket', accessType: 'WRITE', accepts: [ @@ -18,12 +18,12 @@ module.exports = Self => { root: true }, http: { - path: `/changeState`, + path: `/state`, verb: 'POST' } }); - Self.changeState = async(ctx, params, options) => { + Self.state = async(ctx, params, options) => { const models = Self.app.models; const myOptions = {}; let tx; diff --git a/modules/ticket/back/methods/ticket/transferSales.js b/modules/ticket/back/methods/ticket/transferSales.js index 1124f7ec7..54306510c 100644 --- a/modules/ticket/back/methods/ticket/transferSales.js +++ b/modules/ticket/back/methods/ticket/transferSales.js @@ -66,7 +66,7 @@ module.exports = Self => { const ticket = await models.Ticket.findById(id); const canCreateTicket = await models.Client.canCreateTicket(ticket.clientFk); if (!canCreateTicket) - throw new UserError(`You can't create a ticket for a inactive client`); + throw new UserError(`You can't create a ticket for an inactive client`); ticketId = await cloneTicket(originalTicket, myOptions); } @@ -138,14 +138,15 @@ module.exports = Self => { // Update original sale const rest = originalSale.quantity - sale.quantity; query = `UPDATE sale - SET quantity = ? + SET quantity = ?, + originalQuantity = ? WHERE id = ?`; - await Self.rawSql(query, [rest, sale.id], options); + await Self.rawSql(query, [rest, rest, sale.id], options); // Clone sale with new quantity - query = `INSERT INTO sale (itemFk, ticketFk, concept, quantity, originalQuantity, price, discount, priceFixed, + query = `INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, discount, priceFixed, reserved, isPicked, isPriceFixed, isAdded) - SELECT itemFk, ?, concept, ?, originalQuantity, price, discount, priceFixed, + SELECT itemFk, ?, concept, ?, price, discount, priceFixed, reserved, isPicked, isPriceFixed, isAdded FROM sale WHERE id = ?`; diff --git a/modules/ticket/back/methods/ticket/updateDiscount.js b/modules/ticket/back/methods/ticket/updateDiscount.js index e092ee4ed..2e8bec27a 100644 --- a/modules/ticket/back/methods/ticket/updateDiscount.js +++ b/modules/ticket/back/methods/ticket/updateDiscount.js @@ -165,11 +165,10 @@ module.exports = Self => { const salesPerson = ticket.client().salesPersonUser(); if (salesPerson) { - const origin = ctx.req.headers.origin; - + const url = await Self.app.models.Url.getUrl(); const message = $t('Changed sale discount', { ticketId: id, - ticketUrl: `${origin}/#!/ticket/${id}/sale`, + ticketUrl: `${url}ticket/${id}/sale`, changes: changesMade }); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); diff --git a/modules/ticket/back/models/expeditionScan.json b/modules/ticket/back/models/expeditionScan.json index 1db2c1238..7b95eff1e 100644 --- a/modules/ticket/back/models/expeditionScan.json +++ b/modules/ticket/back/models/expeditionScan.json @@ -8,17 +8,16 @@ "properties": { "id": { "type": "number", - "description": "Identifier" + "description": "Identifier", + "id": true }, "expeditionFk": { "type": "number", - "description": "Identifier", - "id": true + "description": "Identifier" }, "palletFk": { "type": "number", - "description": "Identifier", - "id": true + "description": "Identifier" }, "scanned": { "type": "date", diff --git a/modules/ticket/back/models/sale.js b/modules/ticket/back/models/sale.js index ae247fc24..3589eac4b 100644 --- a/modules/ticket/back/models/sale.js +++ b/modules/ticket/back/models/sale.js @@ -1,3 +1,6 @@ +const UserError = require('vn-loopback/util/user-error'); +const LoopBackContext = require('loopback-context'); + module.exports = Self => { require('../methods/sale/getClaimableFromTicket')(Self); require('../methods/sale/reserve')(Self); @@ -9,8 +12,107 @@ module.exports = Self => { require('../methods/sale/refund')(Self); require('../methods/sale/canEdit')(Self); require('../methods/sale/usesMana')(Self); + require('../methods/sale/clone')(Self); Self.validatesPresenceOf('concept', { message: `Concept cannot be blank` }); + + Self.observe('before save', async ctx => { + const models = Self.app.models; + const changes = ctx.data || ctx.instance; + const instance = ctx.currentInstance; + + const newQuantity = changes?.quantity; + if (newQuantity == null) return; + + const loopBackContext = LoopBackContext.getCurrentContext(); + ctx.req = loopBackContext.active; + if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'canForceQuantity', 'WRITE')) return; + + const ticketId = changes?.ticketFk || instance?.ticketFk; + const itemId = changes?.itemFk || instance?.itemFk; + const oldQuantity = instance?.quantity ?? null; + const quantityAdded = newQuantity - oldQuantity; + const isReduction = oldQuantity && newQuantity <= oldQuantity; + + const ticket = await models.Ticket.findById( + ticketId, + { + fields: ['id', 'clientFk', 'warehouseFk', 'addressFk', 'agencyModeFk', 'shipped', 'landed'], + include: { + relation: 'client', + scope: { + fields: ['id', 'clientTypeFk'], + include: { + relation: 'type', + scope: { + fields: ['code', 'description'] + } + } + } + } + }, + ctx.options); + if (ticket?.client()?.type()?.code === 'loses') return; + + const isRefund = await models.TicketRefund.findOne({ + fields: ['id'], + where: {refundTicketFk: ticketId} + }, ctx.options); + if (isRefund) return; + + if (newQuantity < 0) + throw new UserError('You can only add negative amounts in refund tickets'); + + const item = await models.Item.findOne({ + fields: ['family', 'minQuantity'], + where: {id: itemId}, + }, ctx.options); + if (item.family == 'EMB') return; + + if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'isInPreparing', '*')) return; + + await models.Sale.rawSql(`CALL catalog_calcFromItem(?,?,?,?)`, [ + ticket.landed, + ticket.addressFk, + ticket.agencyModeFk, + itemId + ], + ctx.options); + + const [itemInfo] = await models.Sale.rawSql(`SELECT available FROM tmp.ticketCalculateItem`, null, ctx.options); + const available = itemInfo?.available; + + if ((!isReduction && !available) || available < quantityAdded) + throw new UserError(`This item is not available`); + + if (await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*')) return; + + if (newQuantity < item.minQuantity && newQuantity != available) + throw new UserError('The amount cannot be less than the minimum'); + + if (ctx.isNewInstance || isReduction) return; + + const [saleGrouping] = await models.Sale.rawSql(` + SELECT t.price newPrice + FROM tmp.ticketComponentPrice t + ORDER BY (t.grouping <= ?) DESC, t.grouping ASC + LIMIT 1`, + [quantityAdded], + ctx.options); + + await models.Sale.rawSql(` + DROP TEMPORARY TABLE IF EXISTS + tmp.ticketCalculateItem, + tmp.ticketComponentPrice, + tmp.ticketComponent, + tmp.ticketLot, + tmp.zoneGetShipped; + `, null, ctx.options); + + if (!saleGrouping?.newPrice || saleGrouping.newPrice > instance.price) + throw new UserError('The price of the item changed'); + }); }; + diff --git a/modules/ticket/back/models/specs/sale.spec.js b/modules/ticket/back/models/specs/sale.spec.js new file mode 100644 index 000000000..4af44c991 --- /dev/null +++ b/modules/ticket/back/models/specs/sale.spec.js @@ -0,0 +1,361 @@ +/* eslint max-len: ["error", { "code": 150 }]*/ + +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('sale model ', () => { + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + function getActiveCtx(userId) { + return { + active: { + accessToken: {userId}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + } + }; + } + + describe('quantity field ', () => { + it('should add quantity if the quantity is greater than it should be and is role advanced', async() => { + const saleId = 17; + const buyerId = 35; + const ctx = { + req: { + accessToken: {userId: buyerId}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + const tx = await models.Sale.beginTransaction({}); + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(buyerId)); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY + SELECT 100 as available;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + try { + const options = {transaction: tx}; + + const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*'); + + expect(isRoleAdvanced).toEqual(true); + + const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(originalLine.quantity).toEqual(30); + + const newQuantity = originalLine.quantity + 1; + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(modifiedLine.quantity).toEqual(newQuantity); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should update the quantity of a given sale current line', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9)); + + const tx = await models.Sale.beginTransaction({}); + const saleId = 25; + const newQuantity = 4; + + try { + const options = {transaction: tx}; + + const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(originalLine.quantity).toEqual(20); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(modifiedLine.quantity).toEqual(newQuantity); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an error if the quantity is negative and it is not a refund ticket', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const saleId = 17; + const newQuantity = -10; + + const tx = await models.Sale.beginTransaction({}); + + let error; + try { + const options = {transaction: tx}; + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toEqual(new Error('You can only add negative amounts in refund tickets')); + }); + + it('should update a negative quantity when is a ticket refund', async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9)); + + const tx = await models.Sale.beginTransaction({}); + const saleId = 13; + const newQuantity = -10; + + try { + const options = {transaction: tx}; + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(modifiedLine.quantity).toEqual(newQuantity); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an error if the quantity is less than the minimum quantity of the item', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const tx = await models.Sale.beginTransaction({}); + const itemId = 2; + const saleId = 17; + const minQuantity = 30; + const newQuantity = minQuantity - 1; + + let error; + try { + const options = {transaction: tx}; + + const item = await models.Item.findById(itemId, null, options); + await item.updateAttribute('minQuantity', minQuantity, options); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY + SELECT 100 as available;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toEqual(new Error('The amount cannot be less than the minimum')); + }); + + it('should change quantity if has minimum quantity and new quantity is equal than item available', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const tx = await models.Sale.beginTransaction({}); + const itemId = 2; + const saleId = 17; + const minQuantity = 30; + const newQuantity = minQuantity - 1; + + try { + const options = {transaction: tx}; + + const item = await models.Item.findById(itemId, null, options); + await item.updateAttribute('minQuantity', minQuantity, options); + + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY + SELECT ${newQuantity} as available;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + describe('newPrice', () => { + it('should increase quantity if you have enough available and the new price is the same as the previous one', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const tx = await models.Sale.beginTransaction({}); + const itemId = 2; + const saleId = 17; + const minQuantity = 30; + const newQuantity = 31; + + try { + const options = {transaction: tx}; + + const item = await models.Item.findById(itemId, null, options); + await item.updateAttribute('minQuantity', minQuantity, options); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = ` + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available; + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 7.07 as price;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should increase quantity when the new price is lower than the previous one', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const tx = await models.Sale.beginTransaction({}); + const itemId = 2; + const saleId = 17; + const minQuantity = 30; + const newQuantity = 31; + + try { + const options = {transaction: tx}; + + const item = await models.Item.findById(itemId, null, options); + await item.updateAttribute('minQuantity', minQuantity, options); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = ` + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available; + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 1 as price;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw error when increase quantity and the new price is higher than the previous one', async() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1)); + + const tx = await models.Sale.beginTransaction({}); + const itemId = 2; + const saleId = 17; + const minQuantity = 30; + const newQuantity = 31; + + let error; + try { + const options = {transaction: tx}; + + const item = await models.Item.findById(itemId, null, options); + await item.updateAttribute('minQuantity', minQuantity, options); + spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => { + if (sqlStatement.includes('catalog_calcFromItem')) { + sqlStatement = ` + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available; + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 100000 as price;`; + params = null; + } + return models.Ticket.rawSql(sqlStatement, params, options); + }); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).toEqual(new Error('The price of the item changed')); + }); + }); + }); +}); diff --git a/modules/ticket/back/models/ticket-collection.json b/modules/ticket/back/models/ticket-collection.json index e941ac2ce..4bd34f08e 100644 --- a/modules/ticket/back/models/ticket-collection.json +++ b/modules/ticket/back/models/ticket-collection.json @@ -10,6 +10,9 @@ "id": { "id": true, "type": "number" + }, + "usedShelves": { + "type": "number" } }, "relations": { diff --git a/modules/ticket/back/models/ticket-tracking.js b/modules/ticket/back/models/ticket-tracking.js index 2e6d3403e..92e046d3e 100644 --- a/modules/ticket/back/models/ticket-tracking.js +++ b/modules/ticket/back/models/ticket-tracking.js @@ -1,5 +1,4 @@ module.exports = function(Self) { - require('../methods/ticket-tracking/changeState')(Self); require('../methods/ticket-tracking/setDelivered')(Self); Self.validatesPresenceOf('stateFk', {message: 'State cannot be blank'}); diff --git a/modules/ticket/back/models/ticket.js b/modules/ticket/back/models/ticket.js index ea068ef8a..1930765fb 100644 --- a/modules/ticket/back/models/ticket.js +++ b/modules/ticket/back/models/ticket.js @@ -1,4 +1,4 @@ module.exports = Self => { - // Methods require('./ticket-methods')(Self); + require('../methods/ticket/state')(Self); }; diff --git a/modules/ticket/front/advance-search-panel/index.js b/modules/ticket/front/advance-search-panel/index.js index 8ddbe78d4..218fb14d3 100644 --- a/modules/ticket/front/advance-search-panel/index.js +++ b/modules/ticket/front/advance-search-panel/index.js @@ -16,8 +16,8 @@ class Controller extends SearchPanel { this.$http.get('ItemPackingTypes', {filter}).then(res => { for (let ipt of res.data) { itemPackingTypes.push({ - code: ipt.code, - description: this.$t(ipt.description) + description: this.$t(ipt.description), + code: ipt.code }); } this.itemPackingTypes = itemPackingTypes; diff --git a/modules/ticket/front/advance/index.html b/modules/ticket/front/advance/index.html index e6f16c965..a6cf8face 100644 --- a/modules/ticket/front/advance/index.html +++ b/modules/ticket/front/advance/index.html @@ -81,6 +81,9 @@ Liters + + Zone + Not Movable @@ -155,6 +158,7 @@ {{::ticket.futureLiters | dashIfEmpty}} + {{::ticket.futureZoneName | dashIfEmpty}} {{::ticket.notMovableLines | dashIfEmpty}} {{::ticket.futureLines | dashIfEmpty}} diff --git a/modules/ticket/front/advance/index.js b/modules/ticket/front/advance/index.js index 0cec41227..389bcdf14 100644 --- a/modules/ticket/front/advance/index.js +++ b/modules/ticket/front/advance/index.js @@ -15,28 +15,22 @@ export default class Controller extends Section { { field: 'state', searchable: false - }, - { + }, { field: 'futureState', searchable: false - }, - { + }, { field: 'totalWithVat', searchable: false - }, - { + }, { field: 'futureTotalWithVat', searchable: false - }, - { + }, { field: 'shipped', searchable: false - }, - { + }, { field: 'futureShipped', searchable: false - }, - { + }, { field: 'ipt', autocomplete: { url: 'ItemPackingTypes', @@ -44,8 +38,7 @@ export default class Controller extends Section { showField: 'description', valueField: 'code' } - }, - { + }, { field: 'futureIpt', autocomplete: { url: 'ItemPackingTypes', @@ -53,6 +46,11 @@ export default class Controller extends Section { showField: 'description', valueField: 'code' } + }, { + field: 'futureZoneFk', + autocomplete: { + url: 'Zones', + } }, ] }; @@ -158,37 +156,21 @@ export default class Controller extends Section { exprBuilder(param, value) { switch (param) { case 'id': - return {'id': value}; case 'futureId': - return {'futureId': value}; case 'liters': - return {'liters': value}; - case 'lines': - return {'lines': value}; case 'futureLiters': - return {'futureLiters': value}; + case 'lines': case 'futureLines': - return {'futureLines': value}; - case 'ipt': - return {or: - [ - {'ipt': {like: `%${value}%`}}, - {'ipt': null} - ] - }; - case 'futureIpt': - return {or: - [ - {'futureIpt': {like: `%${value}%`}}, - {'futureIpt': null} - ] - }; case 'totalWithVat': - return {'totalWithVat': value}; case 'futureTotalWithVat': - return {'futureTotalWithVat': value}; + case 'futureZone': case 'notMovableLines': - return {'notMovableLines': value}; + case 'futureZoneFk': + return {[param]: value}; + case 'ipt': + return {'ipt': {like: `%${value}%`}}; + case 'futureIpt': + return {'futureIpt': {like: `%${value}%`}}; } } } diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index d1f39fd19..18db8c147 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -201,7 +201,7 @@ class Controller extends Section { sendImportSms() { const params = { ticketId: this.id, - created: this.ticket.updated + shipped: this.ticket.shipped }; this.showSMSDialog({ message: this.$t('Minimum is needed', params) diff --git a/modules/ticket/front/descriptor/locale/en.yml b/modules/ticket/front/descriptor/locale/en.yml index 8eed2265d..698d699d7 100644 --- a/modules/ticket/front/descriptor/locale/en.yml +++ b/modules/ticket/front/descriptor/locale/en.yml @@ -1,3 +1,3 @@ Make a payment: "Verdnatura communicates:\rYour order is pending of payment.\rPlease, enter the web page and make the payment with card.\rThank you." -Minimum is needed: "Verdnatura communicates:\rA minimum import of 50€ (Without BAT) is needed for your order {{ticketId}} from date {{created | date: 'dd/MM/yyyy'}} to receive it with no extra fees." +Minimum is needed: "Verdnatura communicates:\rA minimum import of 50€ (Without BAT) is needed for your order {{ticketId}} from date {{shipped | date: 'dd/MM/yyyy'}} to receive it with no extra fees." Send changes: "Verdnatura communicates:\rOrder {{ticketId}} date {{created | date: 'dd/MM/yyyy'}}\r{{changes}}" diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index 86b1a6b57..b809c1b83 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -3,8 +3,8 @@ Are you sure you want to send it?: ¿Seguro que quieres enviarlo? Show pallet report: Ver hoja de pallet Change shipped hour: Cambiar hora de envío Shipped hour: Hora de envío -Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectue el pago con tarjeta.\rMuchas gracias." -Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales." +Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectúe el pago con tarjeta.\rMuchas gracias." +Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{shipped | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales." Ticket invoiced: Ticket facturado Make invoice: Crear factura Regenerate invoice PDF: Regenerar PDF factura diff --git a/modules/ticket/front/index/locale/es.yml b/modules/ticket/front/index/locale/es.yml index afa3d654e..89828d4d9 100644 --- a/modules/ticket/front/index/locale/es.yml +++ b/modules/ticket/front/index/locale/es.yml @@ -18,3 +18,4 @@ Multiple invoice: Factura múltiple Make invoice...: Crear factura... Invoice selected tickets: Facturar tickets seleccionados Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets? +Rounding: Redondeo diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index b10df317b..262395d16 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -15,7 +15,7 @@ + on-change="$ctrl.state(value)"> - + { - this.currentWorkerMana = res.data; - }); } getUsesMana() { @@ -179,9 +171,9 @@ class Controller extends Section { this.card.reload(); } - changeState(value) { + state(value) { const params = {ticketFk: this.$params.id, code: value}; - return this.$http.post('TicketTrackings/changeState', params).then(() => { + return this.$http.post('Tickets/state', params).then(() => { this.vnApp.showSuccess(this.$t('Data saved!')); this.card.reload(); }).finally(() => this.resetChanges()); diff --git a/modules/ticket/front/sale/index.spec.js b/modules/ticket/front/sale/index.spec.js index 9da8e6e7c..70781eb58 100644 --- a/modules/ticket/front/sale/index.spec.js +++ b/modules/ticket/front/sale/index.spec.js @@ -120,12 +120,10 @@ describe('Ticket', () => { const expectedAmount = 250; $httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount); $httpBackend.expect('GET', 'Sales/usesMana').respond(200); - $httpBackend.expect('GET', 'WorkerManas/getCurrentWorkerMana').respond(200, expectedAmount); controller.getMana(); $httpBackend.flush(); expect(controller.edit.mana).toEqual(expectedAmount); - expect(controller.currentWorkerMana).toEqual(expectedAmount); }); }); @@ -230,15 +228,16 @@ describe('Ticket', () => { }); }); - describe('changeState()', () => { - it('should make an HTTP post query, then call the showSuccess(), reload() and resetChanges() methods', () => { + describe('state()', () => { + it('should make an HTTP post query, then call the showSuccess(),' + + ' reload() and resetChanges() methods', () => { jest.spyOn(controller.card, 'reload').mockReturnThis(); jest.spyOn(controller.vnApp, 'showSuccess').mockReturnThis(); jest.spyOn(controller, 'resetChanges').mockReturnThis(); const expectedParams = {ticketFk: 1, code: 'OK'}; - $httpBackend.expect('POST', `TicketTrackings/changeState`, expectedParams).respond(200); - controller.changeState('OK'); + $httpBackend.expect('POST', `Tickets/state`, expectedParams).respond(200); + controller.state('OK'); $httpBackend.flush(); expect(controller.card.reload).toHaveBeenCalledWith(); @@ -248,7 +247,8 @@ describe('Ticket', () => { }); describe('removeSales()', () => { - it('should make an HTTP post query, then call the showSuccess(), removeSelectedSales() and resetChanges() methods', () => { + it('should make an HTTP post query, then call the showSuccess(),' + + ' removeSelectedSales() and resetChanges() methods', () => { jest.spyOn(controller.vnApp, 'showSuccess').mockReturnThis(); jest.spyOn(controller, 'removeSelectedSales').mockReturnThis(); jest.spyOn(controller, 'resetChanges').mockReturnThis(); @@ -354,7 +354,8 @@ describe('Ticket', () => { }); describe('updatePrice()', () => { - it('should make an HTTP POST query, update the sale price and then call to the resetChanges() method', () => { + it('should make an HTTP POST query, update the sale price ' + + 'and then call to the resetChanges() method', () => { jest.spyOn(controller.vnApp, 'showSuccess').mockReturnThis(); jest.spyOn(controller, 'resetChanges').mockReturnThis(); @@ -420,7 +421,8 @@ describe('Ticket', () => { expect(controller.$.editDiscount.hide).toHaveBeenCalledWith(); }); - it('should not call to the updateDiscount() method and then to the editDiscountDialog hide() method', () => { + it('should not call to the updateDiscount() method and then' + + ' to the editDiscountDialog hide() method', () => { jest.spyOn(controller, 'updateDiscount').mockReturnThis(); const firstSelectedSale = controller.sales[0]; @@ -446,7 +448,8 @@ describe('Ticket', () => { }); describe('updateDiscount()', () => { - it('should make an HTTP POST query, update the sales discount and then call to the resetChanges() method', () => { + it('should make an HTTP POST query, update the sales discount ' + + 'and then call to the resetChanges() method', () => { jest.spyOn(controller, 'resetChanges').mockReturnThis(); jest.spyOn(controller.vnApp, 'showSuccess').mockReturnThis(); diff --git a/modules/ticket/front/services/index.html b/modules/ticket/front/services/index.html index bc288a8a2..5128873c4 100644 --- a/modules/ticket/front/services/index.html +++ b/modules/ticket/front/services/index.html @@ -29,7 +29,7 @@ disabled="watcher.dataChanged() || !$ctrl.checkeds.length" label="Pay" ng-click="$ctrl.createRefund()" - vn-acl="invoicing, claimManager, salesAssistant" + vn-acl="invoicing, claimManager, salesAssistant, buyer" vn-acl-action="remove"> @@ -37,7 +37,9 @@ + disabled="!service.id" + vn-acl="invoicing, claimManager, salesAssistant, buyer" + vn-acl-action="remove"> + step="0.01" + min="0"> + on-change="$ctrl.state(value)"> + > + + > + + ng-if="$ctrl.summary.client.phone != null"> + + ng-if="$ctrl.summary.client.mobile != null"> + @@ -202,7 +212,7 @@ {{::sale.quantity}}
- {{::sale.item.name}} + {{::sale.concept}}

{{::sale.item.subName}}

diff --git a/modules/ticket/front/summary/index.js b/modules/ticket/front/summary/index.js index 60394b5b3..9d161f63e 100644 --- a/modules/ticket/front/summary/index.js +++ b/modules/ticket/front/summary/index.js @@ -59,13 +59,12 @@ class Controller extends Summary { this.$.invoiceOutDescriptor.show(event.target, this.summary.invoiceOut.id); } - changeState(value) { + state(value) { const params = { ticketFk: 'id' in this.ticket ? this.ticket.id : this.$params.id, code: value }; - - this.$http.post(`TicketTrackings/changeState`, params) + this.$http.post(`Tickets/state`, params) .then(() => { if ('id' in this.$params) this.reload(); }) diff --git a/modules/ticket/front/summary/index.spec.js b/modules/ticket/front/summary/index.spec.js index 599da73ae..6837bfd54 100644 --- a/modules/ticket/front/summary/index.spec.js +++ b/modules/ticket/front/summary/index.spec.js @@ -43,15 +43,15 @@ describe('Ticket', () => { }); }); - describe('changeState()', () => { + describe('state()', () => { it('should change the state', () => { jest.spyOn(controller.vnApp, 'showSuccess'); const value = 'myTicketState'; let res = {id: 1, nickname: 'myNickname'}; $httpBackend.when('GET', `Tickets/1/summary`).respond(200, res); - $httpBackend.expectPOST(`TicketTrackings/changeState`).respond(200, 'ok'); - controller.changeState(value); + $httpBackend.expectPOST(`Tickets/state`).respond(200, 'ok'); + controller.state(value); $httpBackend.flush(); expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!'); diff --git a/modules/ticket/front/summary/style.scss b/modules/ticket/front/summary/style.scss index e92ff0d42..2f52de65e 100644 --- a/modules/ticket/front/summary/style.scss +++ b/modules/ticket/front/summary/style.scss @@ -47,4 +47,9 @@ vn-ticket-summary .summary { } } } + + vn-icon.tel { + font-size: 1.1em; + vertical-align: bottom; + } } \ No newline at end of file diff --git a/modules/ticket/front/tracking/edit/index.html b/modules/ticket/front/tracking/edit/index.html index bff8e71b1..47f367007 100644 --- a/modules/ticket/front/tracking/edit/index.html +++ b/modules/ticket/front/tracking/edit/index.html @@ -1,4 +1,4 @@ - + { + this.$http.post(`Tickets/state`, this.params).then(() => { this.$.watcher.updateOriginalData(); this.card.reload(); this.vnApp.showSuccess(this.$t('Data saved!')); diff --git a/modules/ticket/front/tracking/edit/index.spec.js b/modules/ticket/front/tracking/edit/index.spec.js index 1ba5912b5..9d9aa7983 100644 --- a/modules/ticket/front/tracking/edit/index.spec.js +++ b/modules/ticket/front/tracking/edit/index.spec.js @@ -61,7 +61,7 @@ describe('Ticket', () => { jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.$state, 'go'); - $httpBackend.expectPOST(`TicketTrackings/changeState`, controller.params).respond({}); + $httpBackend.expectPOST(`Tickets/state`, controller.params).respond({}); controller.onSubmit(); $httpBackend.flush(); diff --git a/modules/travel/back/methods/travel/extraCommunityFilter.js b/modules/travel/back/methods/travel/extraCommunityFilter.js index 388ba52a1..488297318 100644 --- a/modules/travel/back/methods/travel/extraCommunityFilter.js +++ b/modules/travel/back/methods/travel/extraCommunityFilter.js @@ -132,12 +132,18 @@ module.exports = Self => { s.nickname AS cargoSupplierNickname, s.name AS supplierName, CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg, - CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) as DECIMAL(10,0)) as volumeKg + CAST( + SUM( + vc.aerealVolumetricDensity * + b.stickers * + IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000 + ) as DECIMAL(10,0) + ) as volumeKg FROM travel t LEFT JOIN supplier s ON s.id = t.cargoSupplierFk LEFT JOIN entry e ON e.travelFk = t.id LEFT JOIN buy b ON b.entryFk = e.id - LEFT JOIN packaging pkg ON pkg.id = b.packageFk + LEFT JOIN packaging pkg ON pkg.id = b.packagingFk LEFT JOIN item i ON i.id = b.itemFk LEFT JOIN itemType it ON it.id = i.typeFk JOIN warehouse w ON w.id = t.warehouseInFk @@ -169,11 +175,17 @@ module.exports = Self => { e.evaNotes, e.invoiceAmount, CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) as loadedkg, - CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) AS DECIMAL(10,0)) as volumeKg + CAST( + SUM( + vc.aerealVolumetricDensity * + b.stickers * + IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000 + ) AS DECIMAL(10,0) + ) as volumeKg FROM tmp.travel tr JOIN entry e ON e.travelFk = tr.id JOIN buy b ON b.entryFk = e.id - JOIN packaging pkg ON pkg.id = b.packageFk + JOIN packaging pkg ON pkg.id = b.packagingFk JOIN item i ON i.id = b.itemFk JOIN itemType it ON it.id = i.typeFk JOIN supplier s ON s.id = e.supplierFk diff --git a/modules/travel/back/methods/travel/getEntries.js b/modules/travel/back/methods/travel/getEntries.js index 71bb0d8fb..50088ccfa 100644 --- a/modules/travel/back/methods/travel/getEntries.js +++ b/modules/travel/back/methods/travel/getEntries.js @@ -47,7 +47,7 @@ module.exports = Self => { LEFT JOIN vn.buy b ON b.entryFk = e.id LEFT JOIN vn.supplier s ON e.supplierFk = s.id JOIN vn.item i ON i.id = b.itemFk - LEFT JOIN vn.packaging p ON p.id = b.packageFk + LEFT JOIN vn.packaging p ON p.id = b.packagingFk JOIN vn.packaging pcc ON pcc.id = 'cc' JOIN vn.packaging ppallet ON ppallet.id = 'pallet 100' JOIN vn.packagingConfig pconfig diff --git a/modules/worker/back/methods/worker-mana/getCurrentWorkerMana.js b/modules/worker/back/methods/worker-mana/getCurrentWorkerMana.js deleted file mode 100644 index fa34af475..000000000 --- a/modules/worker/back/methods/worker-mana/getCurrentWorkerMana.js +++ /dev/null @@ -1,26 +0,0 @@ -module.exports = Self => { - Self.remoteMethodCtx('getCurrentWorkerMana', { - description: 'Returns the mana of the logged worker', - accessType: 'READ', - accepts: [], - returns: { - type: 'number', - root: true - }, - http: { - path: `/getCurrentWorkerMana`, - verb: 'GET' - } - }); - - Self.getCurrentWorkerMana = async ctx => { - let userId = ctx.req.accessToken.userId; - - let workerMana = await Self.app.models.WorkerMana.findOne({ - where: {workerFk: userId}, - fields: 'amount' - }); - - return workerMana ? workerMana.amount : 0; - }; -}; diff --git a/modules/worker/back/methods/worker-mana/specs/getCurrentWorkerMana.spec.js b/modules/worker/back/methods/worker-mana/specs/getCurrentWorkerMana.spec.js deleted file mode 100644 index 8d626e720..000000000 --- a/modules/worker/back/methods/worker-mana/specs/getCurrentWorkerMana.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -const app = require('vn-loopback/server/server'); - -describe('workerMana getCurrentWorkerMana()', () => { - it('should get the mana of the logged worker', async() => { - let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 18}}}); - - expect(mana).toEqual(124); - }); - - it('should return 0 if the user doesnt uses mana', async() => { - let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 9}}}); - - expect(mana).toEqual(0); - }); -}); diff --git a/modules/worker/back/methods/worker-time-control/sendMail.js b/modules/worker/back/methods/worker-time-control/sendMail.js index 2c5143612..6f67bbea3 100644 --- a/modules/worker/back/methods/worker-time-control/sendMail.js +++ b/modules/worker/back/methods/worker-time-control/sendMail.js @@ -66,46 +66,36 @@ module.exports = Self => { stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate'); + const destroyAllWhere = { + timed: {between: [started, ended]}, + isSendMail: true + }; + const updateAllWhere = { + year: args.year, + week: args.week + }; + + const tmpUserSQL = ` + CREATE OR REPLACE TEMPORARY TABLE tmp.user + SELECT id as userFk + FROM vn.worker`; + let tmpUser = new ParameterizedSQL(tmpUserSQL); + if (args.workerId) { - await models.WorkerTimeControl.destroyAll({ - userFk: args.workerId, - timed: {between: [started, ended]}, - isSendMail: true - }, myOptions); - - const where = { - workerFk: args.workerId, - year: args.year, - week: args.week - }; - await models.WorkerTimeControlMail.updateAll(where, { - updated: Date.vnNew(), state: 'SENDED' - }, myOptions); - - stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`'); - stmts.push(stmt); - stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE tmp.`user` SELECT id userFk FROM account.user WHERE id = ?', [args.workerId]); - stmts.push(stmt); - } else { - await models.WorkerTimeControl.destroyAll({ - timed: {between: [started, ended]}, - isSendMail: true - }, myOptions); - - const where = { - year: args.year, - week: args.week - }; - await models.WorkerTimeControlMail.updateAll(where, { - updated: Date.vnNew(), state: 'SENDED' - }, myOptions); - - stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`'); - stmts.push(stmt); - stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE IF NOT EXISTS tmp.`user` SELECT id as userFk FROM vn.worker w JOIN account.`user` u ON u.id = w.id WHERE id IS NOT NULL'); - stmts.push(stmt); + destroyAllWhere.userFk = args.workerId; + updateAllWhere.workerFk = args.workerId; + tmpUser = new ParameterizedSQL(tmpUserSQL + ' WHERE id = ?', [args.workerId]); } + await models.WorkerTimeControl.destroyAll(destroyAllWhere, myOptions); + + await models.WorkerTimeControlMail.updateAll(updateAllWhere, { + updated: Date.vnNew(), + state: 'SENDED' + }, myOptions); + + stmts.push(tmpUser); + stmt = new ParameterizedSQL( `CALL vn.timeControl_calculate(?, ?) `, [started, ended]); diff --git a/modules/worker/back/methods/worker-time-control/specs/timeEntry.spec.js b/modules/worker/back/methods/worker-time-control/specs/timeEntry.spec.js index e90c849b7..42ec6290a 100644 --- a/modules/worker/back/methods/worker-time-control/specs/timeEntry.spec.js +++ b/modules/worker/back/methods/worker-time-control/specs/timeEntry.spec.js @@ -14,7 +14,6 @@ describe('workerTimeControl add/delete timeEntry()', () => { const tuesday = 2; const thursday = 4; const friday = 5; - const saturday = 6; const sunday = 7; const activeCtx = { accessToken: {userId: 50}, @@ -200,15 +199,15 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); describe('WorkerTimeControl_clockIn calls', () => { - it('should fail to add a time entry if the target user has an absence that day', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); + let workerId; + beforeEach(() => { activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; + workerId = hankPymId; + }); + it('should fail to add a time entry if the target user has an absence that day', async() => { const date = Date.vnNew(); - date.setDate(date.getDate() - 16); date.setHours(8, 0, 0); - let error; - + date.setDate(date.getDate() - 16); const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; try { @@ -225,15 +224,12 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); it('should fail to add a time entry for a worker without an existing contract', async() => { - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; const date = Date.vnNew(); date.setFullYear(date.getFullYear() - 2); - let error; const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; try { - const options = {transaction: tx}; ctx.args = {timed: date, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); @@ -246,19 +242,39 @@ describe('workerTimeControl add/delete timeEntry()', () => { expect(error.message).toBe(`No hay un contrato en vigor`); }); + it('should fail to add a time entry for a worker without an existing contract', async() => { + let date = Date.vnNew(); + date.setDate(date.getDate() - 2); + let error; + + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + date.setHours(0, 0, 0); + ctx.args = {timed: date, direction: 'in'}; + await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); + + try { + date.setHours(20,0, 1); + ctx.args = {timed: date, direction: 'out'}; + await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`); + }); + describe('direction errors', () => { + let date = Date.vnNew(); + date.setDate(date.getDate() - 1); + let error; it('should throw an error when trying "in" direction twice', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); - date = weekDay(date, monday); - let error; - const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; + date.setHours(8, 0, 0); ctx.args = {timed: date, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); @@ -278,21 +294,13 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); - date = weekDay(date, monday); - let error; - const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; date.setHours(8, 0, 0); ctx.args = {timed: date, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); + date.setHours(9, 0, 0); ctx.args = {timed: date, direction: 'middle'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); @@ -312,15 +320,6 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); it('Should throw an error when trying "out" before closing a "middle" couple', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); - date = weekDay(date, monday); - let error; - const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; @@ -346,15 +345,6 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); it('should throw an error when trying "middle" after "out"', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); - date = weekDay(date, monday); - let error; - const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; @@ -380,15 +370,6 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); it('should throw an error when trying "out" direction twice', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); - date = weekDay(date, monday); - let error; - const tx = await models.WorkerTimeControl.beginTransaction({}); const options = {transaction: tx}; @@ -415,14 +396,12 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); describe('12h rest', () => { + activeCtx.accessToken.userId = salesBossId; + const workerId = hankPymId; it('should throw an error when the 12h rest is not fulfilled yet', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; let date = Date.vnNew(); - date.setDate(date.getDate() - 21); + date.setDate(date.getDate() - 2); date = weekDay(date, monday); let error; @@ -448,16 +427,12 @@ describe('workerTimeControl add/delete timeEntry()', () => { error = e; } - expect(error.message).toBe(`Descanso diario 12h.`); + expect(error.message).toBe(`Descanso diario`); }); it('should not fail as the 12h rest is fulfilled', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - let date = Date.vnNew(); - date.setDate(date.getDate() - 21); + date.setDate(date.getDate() - 2); date = weekDay(date, monday); let error; @@ -488,13 +463,12 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); describe('for 3500kg drivers with enforced 9h rest', () => { + activeCtx.accessToken.userId = salesBossId; + const workerId = jessicaJonesId; it('should throw an error when the 9h enforced rest is not fulfilled', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = jessicaJonesId; let date = Date.vnNew(); - date.setDate(date.getDate() - 21); + date.setDate(date.getDate() - 2); date = weekDay(date, monday); let error; @@ -520,16 +494,13 @@ describe('workerTimeControl add/delete timeEntry()', () => { error = e; } - expect(error.message).toBe(`Descanso diario 9h.`); + expect(error.message).toBe(`Descanso diario`); }); it('should not fail when the 9h enforced rest is fulfilled', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = jessicaJonesId; - + let date = Date.vnNew(); - date.setDate(date.getDate() - 21); + date.setDate(date.getDate() - 2); date = weekDay(date, monday); let error; @@ -559,14 +530,11 @@ describe('workerTimeControl add/delete timeEntry()', () => { }); }); - describe('for 36h weekly rest', () => { - it('should throw an error when the 36h weekly rest is not fulfilled', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - + describe('for 72h weekly rest', () => { + + it('should throw an error when work 11 consecutive days', async() => { let date = Date.vnNew(); - date.setMonth(date.getMonth() - 2); + date.setMonth(date.getMonth() - 1); date.setDate(1); let error; @@ -576,66 +544,24 @@ describe('workerTimeControl add/delete timeEntry()', () => { await populateWeek(date, monday, sunday, ctx, workerId, options); date = nextWeek(date); await populateWeek(date, monday, thursday, ctx, workerId, options); - date = weekDay(date, friday); - date.setHours(7, 59, 0); - ctx.args = {timed: date, direction: 'in'}; - await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); - try { - date.setHours(8, 1, 0); - ctx.args = {timed: date, direction: 'out'}; - await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - error = e; - } - - expect(error.message).toBe(`Descanso semanal 36h. / 72h.`); - }); - - it('should throw an error when the 36h weekly rest is not fulfilled again', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; - - let date = Date.vnNew(); - date.setMonth(date.getMonth() - 2); - date.setDate(1); - let error; - - const tx = await models.WorkerTimeControl.beginTransaction({}); - const options = {transaction: tx}; - - await populateWeek(date, monday, sunday, ctx, workerId, options); - date = nextWeek(date); - await populateWeek(date, monday, thursday, ctx, workerId, options); - - try { - date = weekDay(date, saturday); - date.setHours(3, 59, 0); + date = weekDay(date, friday); + date.setHours(10, 0, 1); ctx.args = {timed: date, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); - await tx.rollback(); } catch (e) { await tx.rollback(); error = e; } - expect(error.message).toBe(`Descanso semanal 36h. / 72h.`); + expect(error.message).toBe(`Descanso semanal`); }); - }); - describe('for 72h weekly rest', () => { - it('should throw when the 72h weekly rest is not fulfilled yet', async() => { - pending('https://redmine.verdnatura.es/issues/4707'); - activeCtx.accessToken.userId = salesBossId; - const workerId = hankPymId; + it('should throw an error when the 72h weekly rest is not fulfilled', async() => { let date = Date.vnNew(); - date.setMonth(date.getMonth() - 2); + date.setMonth(date.getMonth() - 1); date.setDate(1); let error; @@ -645,32 +571,263 @@ describe('workerTimeControl add/delete timeEntry()', () => { await populateWeek(date, monday, sunday, ctx, workerId, options); date = nextWeek(date); await populateWeek(date, monday, thursday, ctx, workerId, options); - date = nextWeek(date); - await populateWeek(date, monday, friday, ctx, workerId, options); - date = nextWeek(date); - await populateWeek(date, monday, saturday, ctx, workerId, options); - date = nextWeek(date); - await populateWeek(date, monday, saturday, ctx, workerId, options); - date = lastWeek(date); try { date = weekDay(date, sunday); - date.setHours(8, 0, 0); + date.setHours(17, 59, 0); ctx.args = {timed: date, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); - await tx.rollback(); } catch (e) { await tx.rollback(); error = e; } - expect(error.message).toBe(`Descanso semanal 36h. / 72h.`); + expect(error.message).toBe(`Descanso semanal`); + }); + + it('should throw an error when the 72h weekly rest is fulfilled', async() => { + + let date = Date.vnNew(); + date.setMonth(date.getMonth() - 1); + date.setDate(1); + let error; + + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + await populateWeek(date, monday, sunday, ctx, workerId, options); + date = nextWeek(date); + await populateWeek(date, monday, thursday, ctx, workerId, options); + + try { + date = weekDay(date, sunday); + date.setHours(18, 00, 0); + ctx.args = {timed: date, direction: 'in'}; + await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error).not.toBeDefined; + }); + }); + + describe('WorkerTimeControl_calculate calls', () => { + let dated = Date.vnNew(); + dated.setDate(dated.getDate() - 7); + dated = new Date(weekDay(dated, monday)); + const end = new Date(dated); + end.setDate(end.getDate() + 1); + + it(`should return today's worked 8 hours without break`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28800); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 15min break and work time consecutive less than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(14, 59, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(15, 14, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28800); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 15min break and work time consecutive greater than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(15, 0, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(15, 15, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28800); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 25min break and work time consecutive less than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(14, 59, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(15, 24, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28500); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 25min break and work time consecutive greater than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(15, 0, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(15, 25, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(28500); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 60min break and work time consecutive less than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(14, 59, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(15, 59, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(25200); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it(`should return today's worked hours with 60min break and work time consecutive greater than 5h`, async() => { + const tx = await models.WorkerTimeControl.beginTransaction({}); + const options = {transaction: tx}; + + try { + await populateWeek(dated, monday, monday, ctx, hankPymId, options); + dated.setHours(15, 0, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + dated.setHours(16, 0, 0); + await addTimeEntry(ctx, dated, 'middle', hankPymId, options); + + const start = new Date(dated - 1); + start.setHours(0, 0, 0); + await models.WorkerTimeControl.rawSql('CALL vn.timeControl_calculateByUser(?, ?, ?)', [ + hankPymId, + start, + end + ], options); + + let [timeControlCalculateTable] = await models.WorkerTimeControl.rawSql('SELECT * FROM tmp.timeControlCalculate', null, options); + + expect(timeControlCalculateTable.timeWorkSeconds).toEqual(26400); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } }); }); }); }); +async function addTimeEntry(ctx, dated, direction, userId, option) { + ctx.args = {timed: dated, direction}; + await models.WorkerTimeControl.addTimeEntry(ctx, userId, option); +} + function weekDay(date, dayToSet) { const currentDay = date.getDay(); const distance = dayToSet - currentDay; @@ -704,10 +861,10 @@ async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) { dateEnd.setDate(dateStart.getDate() + dayEnd); for (let i = dayStart; i <= dayEnd; i++) { - dateStart.setHours(8, 0, 0); + dateStart.setHours(10, 0, 0); ctx.args = {timed: dateStart, direction: 'in'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); - dateStart.setHours(16, 0, 0); + dateStart.setHours(18, 0, 0); ctx.args = {timed: dateStart, direction: 'out'}; await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options); dateStart.setDate(dateStart.getDate() + 1); diff --git a/modules/worker/back/methods/worker-time-control/specs/updateWorkerTimeControlMail.spec.js b/modules/worker/back/methods/worker-time-control/specs/updateWorkerTimeControlMail.spec.js new file mode 100644 index 000000000..3b5b2b73f --- /dev/null +++ b/modules/worker/back/methods/worker-time-control/specs/updateWorkerTimeControlMail.spec.js @@ -0,0 +1,103 @@ +const models = require('vn-loopback/server/server').models; + +describe('updateWorkerTimeControlMail()', () => { + it('should update WorkerTimeControlMail if exist record', async() => { + const tx = await models.WorkerTimeControlMail.beginTransaction({}); + const args = { + workerId: 9, + week: 50, + year: 2000, + state: 'CONFIRMED' + }; + const ctx = {args}; + + try { + const options = {transaction: tx}; + const beforeMail = await models.WorkerTimeControlMail.findOne({ + where: { + workerFk: args.workerId, + year: args.year, + week: args.week, + } + }, options); + await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options); + const afterMail = await models.WorkerTimeControlMail.findOne({ + where: { + workerFk: args.workerId, + year: args.year, + week: args.week, + } + }, options); + + expect(beforeMail.state).toEqual('SENDED'); + expect(afterMail.state).toEqual(args.state); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should insert WorkerTimeControlMail if exist record', async() => { + const tx = await models.WorkerTimeControlMail.beginTransaction({}); + const args = { + workerId: 1, + week: 51, + year: 2000, + state: 'SENDED' + }; + const ctx = {args}; + + try { + const options = {transaction: tx}; + const beforeMail = await models.WorkerTimeControlMail.find({ + where: { + workerFk: args.workerId, + year: args.year, + week: args.week, + } + }, options); + await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options); + const afterMail = await models.WorkerTimeControlMail.find({ + where: { + workerFk: args.workerId, + year: args.year, + week: args.week, + } + }, options); + + expect(beforeMail).toEqual([]); + expect(afterMail.length).toEqual(1); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw error if not exist any record in this week', async() => { + const tx = await models.WorkerTimeControlMail.beginTransaction({}); + const ctx = {args: { + workerId: 1, + week: 1, + year: 0, + state: 'SENDED' + }}; + + let error; + try { + const options = {transaction: tx}; + await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + error = e; + } + + expect(error.message).toEqual(`There aren't records for this week`); + }); +}); + diff --git a/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js index 6fe30de91..3594f05fe 100644 --- a/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js +++ b/modules/worker/back/methods/worker-time-control/updateWorkerTimeControlMail.js @@ -40,30 +40,39 @@ module.exports = Self => { Self.updateWorkerTimeControlMail = async(ctx, options) => { const models = Self.app.models; const args = ctx.args; - const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({ + const [sent] = await models.WorkerTimeControlMail.find({ where: { - workerFk: args.workerId, year: args.year, - week: args.week - } + week: args.week, + }, + limit: 1 }, myOptions); - if (!workerTimeControlMail) throw new UserError(`There aren't records for this week`); + if (!sent) throw new UserError(`There aren't records for this week`); - await workerTimeControlMail.updateAttributes({ - state: args.state, - reason: args.reason || null - }, myOptions); + const workerTimeControlMail = await models.WorkerTimeControlMail.upsertWithWhere( + { + year: args.year, + week: args.week, + workerFk: args.workerId + }, + { + state: args.state, + reason: args.workerId, + year: args.year, + week: args.week, + workerFk: args.workerId + }, + myOptions); if (args.state == 'SENDED') { await workerTimeControlMail.updateAttributes({ - sendedCounter: workerTimeControlMail.sendedCounter + 1 + sendedCounter: workerTimeControlMail.sendedCounter ? workerTimeControlMail.sendedCounter + 1 : 1 }, myOptions); } }; diff --git a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js index 3203dea82..816a1d22b 100644 --- a/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js +++ b/modules/worker/back/methods/worker-time-control/weeklyHourRecordEmail.js @@ -61,9 +61,8 @@ module.exports = Self => { const url = `${salix.url}worker/${args.workerId}/time-control?timestamp=${timestamp}`; ctx.args.url = url; - await Self.sendTemplate(ctx, 'weekly-hour-record'); - - return models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions); + await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions); + return Self.sendTemplate(ctx, 'weekly-hour-record'); }; function getMondayDateFromYearWeek(yearNumber, weekNumber) { diff --git a/modules/worker/back/methods/worker/createAbsence.js b/modules/worker/back/methods/worker/createAbsence.js index cb2cf8330..d628d0a2b 100644 --- a/modules/worker/back/methods/worker/createAbsence.js +++ b/modules/worker/back/methods/worker/createAbsence.js @@ -109,13 +109,13 @@ module.exports = Self => { const absenceType = await models.AbsenceType.findById(args.absenceTypeId, null, myOptions); const account = await models.VnUser.findById(userId, null, myOptions); const subordinated = await models.VnUser.findById(id, null, myOptions); - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const body = $t('Created absence', { author: account.nickname, employee: subordinated.nickname, absenceType: absenceType.name, dated: formatDate(args.dated), - workerUrl: `${origin}/#!/worker/${id}/calendar` + workerUrl: `${url}worker/${id}/calendar` }); await models.Mail.create({ subject: $t('Absence change notification on the labour calendar'), diff --git a/modules/worker/back/methods/worker/deleteAbsence.js b/modules/worker/back/methods/worker/deleteAbsence.js index c315f5178..b71d077a4 100644 --- a/modules/worker/back/methods/worker/deleteAbsence.js +++ b/modules/worker/back/methods/worker/deleteAbsence.js @@ -60,13 +60,13 @@ module.exports = Self => { const absenceType = await models.AbsenceType.findById(absence.dayOffTypeFk, null, myOptions); const account = await models.VnUser.findById(userId, null, myOptions); const subordinated = await models.VnUser.findById(labour.workerFk, null, myOptions); - const origin = ctx.req.headers.origin; + const url = await Self.app.models.Url.getUrl(); const body = $t('Deleted absence', { author: account.nickname, employee: subordinated.nickname, absenceType: absenceType.name, dated: formatDate(absence.dated), - workerUrl: `${origin}/#!/worker/${id}/calendar` + workerUrl: `${url}worker/${id}/calendar` }); await models.Mail.create({ subject: $t('Absence change notification on the labour calendar'), diff --git a/modules/worker/back/methods/worker/new.js b/modules/worker/back/methods/worker/new.js index 199a3be62..5316daf01 100644 --- a/modules/worker/back/methods/worker/new.js +++ b/modules/worker/back/methods/worker/new.js @@ -223,19 +223,16 @@ module.exports = Self => { const user = await models.VnUser.findById(client.id, null, myOptions); await user.updateAttribute('email', args.email, myOptions); - await models.Worker.rawSql( - 'CALL vn.workerCreate(?, ?, ?, ?, ?, ?, ?)', - [ - args.firstName, - args.lastNames, - args.code, - args.bossFk, - client.id, - args.fi, - args.birth, - ], - myOptions - ); + await models.Worker.create({ + id: client.id, + code: args.code, + firstName: args.firstName, + lastName: args.lastNames, + bossFk: args.bossFk, + fi: args.fi, + birth: args.birth, + + }, myOptions); if (tx) await tx.commit(); } catch (error) { diff --git a/modules/worker/back/methods/worker/search.js b/modules/worker/back/methods/worker/search.js index cd0a466ea..7fe9e0666 100644 --- a/modules/worker/back/methods/worker/search.js +++ b/modules/worker/back/methods/worker/search.js @@ -46,7 +46,7 @@ module.exports = Self => { SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk FROM worker w JOIN account.user u ON u.id = w.id - JOIN business b ON b.workerFk = w.id + LEFT JOIN business b ON b.workerFk = w.id ) w`); stmt.merge(conn.makeSuffix(filter)); diff --git a/modules/worker/back/methods/worker/setPassword.js b/modules/worker/back/methods/worker/setPassword.js new file mode 100644 index 000000000..43d3d946f --- /dev/null +++ b/modules/worker/back/methods/worker/setPassword.js @@ -0,0 +1,48 @@ +const UserError = require('vn-loopback/util/user-error'); +module.exports = Self => { + Self.remoteMethodCtx('setPassword', { + description: 'Set a new password', + accepts: [ + { + arg: 'workerFk', + type: 'number', + required: true, + description: 'The worker id', + }, + { + arg: 'newPass', + type: 'String', + required: true, + description: 'The new worker password' + } + ], + http: { + path: `/:id/setPassword`, + verb: 'PATCH' + } + }); + Self.setPassword = async(ctx, options) => { + const models = Self.app.models; + const myOptions = {}; + const {args} = ctx; + let tx; + if (typeof options == 'object') + Object.assign(myOptions, options); + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + try { + const isSubordinate = await models.Worker.isSubordinate(ctx, args.workerFk, myOptions); + if (!isSubordinate) throw new UserError('You don\'t have enough privileges.'); + + await models.VnUser.setPassword(args.workerFk, args.newPass, myOptions); + await models.VnUser.updateAll({id: args.workerFk}, {emailVerified: true}, myOptions); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/worker/back/methods/worker/specs/setPassword.spec.js b/modules/worker/back/methods/worker/specs/setPassword.spec.js new file mode 100644 index 000000000..fbb403b24 --- /dev/null +++ b/modules/worker/back/methods/worker/specs/setPassword.spec.js @@ -0,0 +1,61 @@ +const UserError = require('vn-loopback/util/user-error'); + +const models = require('vn-loopback/server/server').models; + +describe('worker setPassword()', () => { + let ctx; + beforeAll(() => { + ctx = { + req: { + accessToken: {}, + headers: {origin: 'http://localhost'} + }, + args: {workerFk: 9} + }; + }); + + beforeEach(() => { + ctx.req.accessToken.userId = 20; + ctx.args.newPass = 'H3rn4d3z#'; + }); + + it('should change the password', async() => { + const tx = await models.Worker.beginTransaction({}); + + try { + const options = {transaction: tx}; + await models.Worker.setPassword(ctx, options); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should throw an error: Password does not meet requirements', async() => { + const tx = await models.Collection.beginTransaction({}); + ctx.args.newPass = 'Hi'; + try { + const options = {transaction: tx}; + await models.Worker.setPassword(ctx, options); + await tx.rollback(); + } catch (e) { + expect(e.sqlMessage).toEqual('Password does not meet requirements'); + await tx.rollback(); + } + }); + + it('should throw an error: You don\'t have enough privileges.', async() => { + ctx.req.accessToken.userId = 5; + const tx = await models.Collection.beginTransaction({}); + try { + const options = {transaction: tx}; + await models.Worker.setPassword(ctx, options); + await tx.rollback(); + } catch (e) { + expect(e).toEqual(new UserError(`You don't have enough privileges.`)); + await tx.rollback(); + } + }); +}); diff --git a/modules/worker/back/models/operator.json b/modules/worker/back/models/operator.json index 2417078e1..6da3945fc 100644 --- a/modules/worker/back/models/operator.json +++ b/modules/worker/back/models/operator.json @@ -33,6 +33,16 @@ "type": "belongsTo", "model": "Sector", "foreignKey": "sectorFk" + }, + "train": { + "type": "belongsTo", + "model": "Train", + "foreignKey": "trainFk" + }, + "printer": { + "type": "belongsTo", + "model": "Printer", + "foreignKey": "labelerFk" } } } diff --git a/modules/worker/back/models/worker-mana.js b/modules/worker/back/models/worker-mana.js deleted file mode 100644 index 99a0f7694..000000000 --- a/modules/worker/back/models/worker-mana.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = Self => { - require('../methods/worker-mana/getCurrentWorkerMana')(Self); -}; diff --git a/modules/worker/back/models/worker.js b/modules/worker/back/models/worker.js index ccae3a6e6..985d83e9f 100644 --- a/modules/worker/back/models/worker.js +++ b/modules/worker/back/models/worker.js @@ -18,6 +18,7 @@ module.exports = Self => { require('../methods/worker/allocatePDA')(Self); require('../methods/worker/search')(Self); require('../methods/worker/isAuthorized')(Self); + require('../methods/worker/setPassword')(Self); Self.validatesUniquenessOf('locker', { message: 'This locker has already been assigned' diff --git a/modules/worker/back/models/worker.json b/modules/worker/back/models/worker.json index bea86d14d..1a777fffe 100644 --- a/modules/worker/back/models/worker.json +++ b/modules/worker/back/models/worker.json @@ -44,6 +44,18 @@ }, "code": { "type" : "string" + }, + "locker": { + "type" : "number" + }, + "fi": { + "type" : "string" + }, + "birth": { + "type" : "date" + }, + "isF11Allowed": { + "type" : "boolean" } }, "relations": { diff --git a/modules/worker/front/card/index.js b/modules/worker/front/card/index.js index b8b533c5d..9a40e31c2 100644 --- a/modules/worker/front/card/index.js +++ b/modules/worker/front/card/index.js @@ -8,7 +8,7 @@ class Controller extends ModuleCard { { relation: 'user', scope: { - fields: ['name'], + fields: ['name', 'emailVerified'], include: { relation: 'emailUser', scope: { diff --git a/modules/worker/front/descriptor/index.html b/modules/worker/front/descriptor/index.html index 05e883533..aa6b80300 100644 --- a/modules/worker/front/descriptor/index.html +++ b/modules/worker/front/descriptor/index.html @@ -11,6 +11,9 @@ ? 'Click to allow the user to be disabled' : 'Click to exclude the user from getting disabled'}} + + Change password +
@@ -28,11 +31,17 @@ + > + + > +
@@ -66,4 +75,29 @@ - + + + + + + + + + + + + diff --git a/modules/worker/front/descriptor/index.js b/modules/worker/front/descriptor/index.js index 0214f8500..13ffa6f2f 100644 --- a/modules/worker/front/descriptor/index.js +++ b/modules/worker/front/descriptor/index.js @@ -1,6 +1,6 @@ import ngModule from '../module'; import Descriptor from 'salix/components/descriptor'; - +const UserError = require('vn-loopback/util/user-error'); class Controller extends Descriptor { constructor($element, $, $rootScope) { super($element, $); @@ -13,9 +13,11 @@ class Controller extends Descriptor { set worker(value) { this.entity = value; - if (value) this.getIsExcluded(); + + if (this.entity && !this.entity.user.emailVerified) + this.getPassRequirements(); } getIsExcluded() { @@ -39,7 +41,7 @@ class Controller extends Descriptor { { relation: 'user', scope: { - fields: ['name'], + fields: ['name', 'emailVerified'], include: { relation: 'emailUser', scope: { @@ -67,10 +69,29 @@ class Controller extends Descriptor { } ] }; - return this.getData(`Workers/${this.id}`, {filter}) .then(res => this.entity = res.data); } + + getPassRequirements() { + this.$http.get('UserPasswords/findOne') + .then(res => { + this.passRequirements = res.data; + }); + } + + setPassword() { + if (!this.newPassword) + throw new UserError(`You must enter a new password`); + if (this.newPassword != this.repeatPassword) + throw new UserError(`Passwords don't match`); + this.$http.patch( + `Workers/${this.entity.id}/setPassword`, + {workerFk: this.entity.id, newPass: this.newPassword} + ) .then(() => { + this.vnApp.showSuccess(this.$translate.instant('Password changed!')); + }); + } } Controller.$inject = ['$element', '$scope', '$rootScope']; diff --git a/modules/worker/front/descriptor/index.spec.js b/modules/worker/front/descriptor/index.spec.js index dfb800415..d158a9e8e 100644 --- a/modules/worker/front/descriptor/index.spec.js +++ b/modules/worker/front/descriptor/index.spec.js @@ -23,4 +23,24 @@ describe('vnWorkerDescriptor', () => { expect(controller.worker).toEqual(response); }); }); + + describe('setPassword()', () => { + it('should throw an error: You must enter a new password', () => { + try { + controller.setPassword(); + } catch (error) { + expect(error.message).toEqual('You must enter a new password'); + } + }); + + it('should throw an error: Passwords don\'t match', () => { + controller.newPassword = 'aaa'; + controller.repeatPassword = 'bbb'; + try { + controller.setPassword(); + } catch (error) { + expect(error.message).toEqual('Passwords don\'t match'); + } + }); + }); }); diff --git a/modules/worker/front/index.js b/modules/worker/front/index.js index 8fad2c0df..5c03dc8de 100644 --- a/modules/worker/front/index.js +++ b/modules/worker/front/index.js @@ -20,4 +20,5 @@ import './dms/create'; import './dms/edit'; import './note/index'; import './note/create'; +import './notifications'; diff --git a/modules/worker/front/notifications/index.html b/modules/worker/front/notifications/index.html new file mode 100644 index 000000000..7fb3b870e --- /dev/null +++ b/modules/worker/front/notifications/index.html @@ -0,0 +1,2 @@ + + diff --git a/modules/worker/front/notifications/index.js b/modules/worker/front/notifications/index.js new file mode 100644 index 000000000..622892979 --- /dev/null +++ b/modules/worker/front/notifications/index.js @@ -0,0 +1,21 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + } + + async $onInit() { + const url = await this.vnApp.getUrl(`worker/${this.$params.id}/notifications`); + window.open(url).focus(); + } +} + +ngModule.vnComponent('vnWorkerNotifications', { + template: require('./index.html'), + controller: Controller, + bindings: { + ticket: '<' + } +}); diff --git a/modules/worker/front/routes.json b/modules/worker/front/routes.json index 7906b1f45..489b4346a 100644 --- a/modules/worker/front/routes.json +++ b/modules/worker/front/routes.json @@ -15,6 +15,7 @@ {"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.pda", "icon": "phone_android"}, + {"state": "worker.card.notifications", "icon": "notifications"}, {"state": "worker.card.pbx", "icon": "icon-pbx"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"}, { @@ -112,6 +113,14 @@ "params": { "worker": "$ctrl.worker" } + }, { + "url": "/notifications", + "state": "worker.card.notifications", + "component": "vn-worker-notifications", + "description": "Notifications", + "params": { + "worker": "$ctrl.worker" + } }, { "url": "/time-control?timestamp", "state": "worker.card.timeControl", diff --git a/modules/worker/front/summary/index.html b/modules/worker/front/summary/index.html index 0f34f4df8..ff464aa79 100644 --- a/modules/worker/front/summary/index.html +++ b/modules/worker/front/summary/index.html @@ -42,14 +42,21 @@ {{::worker.boss.name}} - + + - + + + > + diff --git a/modules/worker/front/summary/index.js b/modules/worker/front/summary/index.js index f1e4b8b59..0e7f46c7c 100644 --- a/modules/worker/front/summary/index.js +++ b/modules/worker/front/summary/index.js @@ -1,6 +1,5 @@ import ngModule from '../module'; import Summary from 'salix/components/summary'; - class Controller extends Summary { get worker() { return this._worker; diff --git a/modules/worker/front/time-control/index.html b/modules/worker/front/time-control/index.html index b77acbddc..c34a1e3ca 100644 --- a/modules/worker/front/time-control/index.html +++ b/modules/worker/front/time-control/index.html @@ -79,30 +79,32 @@ - + +
+ + + + + + +
- - - - - - + vn-tooltip="{{$ctrl.state ? 'Resend' : 'Send'}} email of this week to the user" + ng-if="$ctrl.isHr && $ctrl.state != 'CONFIRMED' && $ctrl.canResend">
diff --git a/modules/worker/front/time-control/index.js b/modules/worker/front/time-control/index.js index 71de1cef4..0a955c586 100644 --- a/modules/worker/front/time-control/index.js +++ b/modules/worker/front/time-control/index.js @@ -53,6 +53,8 @@ class Controller extends Section { set worker(value) { this._worker = value; this.fetchHours(); + if (this.date) + this.getWeekData(); } /** @@ -109,8 +111,10 @@ class Controller extends Section { dayIndex.setDate(dayIndex.getDate() + 1); } - if (!this.weekTotalHours) this.fetchHours(); - this.getWeekData(); + if (this.worker) { + this.fetchHours(); + this.getWeekData(); + } } set weekTotalHours(totalHours) { @@ -127,17 +131,34 @@ class Controller extends Section { workerFk: this.$params.id, year: this._date.getFullYear(), week: this.getWeekNumber(this._date) - } + }, }; this.$http.get('WorkerTimeControlMails', {filter}) .then(res => { - const mail = res.data; - if (!mail.length) { + if (!res.data.length) { this.state = null; return; } - this.state = mail[0].state; - this.reason = mail[0].reason; + const [mail] = res.data; + this.state = mail.state; + this.reason = mail.reason; + }); + this.canBeResend(); + } + + canBeResend() { + this.canResend = false; + const filter = { + where: { + year: this._date.getFullYear(), + week: this.getWeekNumber(this._date) + }, + limit: 1 + }; + this.$http.get('WorkerTimeControlMails', {filter}) + .then(res => { + if (res.data.length) + this.canResend = true; }); } @@ -356,30 +377,25 @@ class Controller extends Section { } isSatisfied() { - const params = { - workerId: this.worker.id, - year: this.date.getFullYear(), - week: this.weekNumber, - state: 'CONFIRMED' - }; - const query = `WorkerTimeControls/updateWorkerTimeControlMail`; - this.$http.post(query, params).then(() => { - this.getMailStates(this.date); - this.getWeekData(); - this.vnApp.showSuccess(this.$t('Data saved!')); - }); + this.updateWorkerTimeControlMail('CONFIRMED'); } isUnsatisfied() { if (!this.reason) throw new UserError(`You must indicate a reason`); + this.updateWorkerTimeControlMail('REVISE', this.reason); + } + updateWorkerTimeControlMail(state, reason) { const params = { workerId: this.worker.id, year: this.date.getFullYear(), week: this.weekNumber, - state: 'REVISE', - reason: this.reason + state }; + + if (reason) + params.reason = reason; + const query = `WorkerTimeControls/updateWorkerTimeControlMail`; this.$http.post(query, params).then(() => { this.getMailStates(this.date); @@ -388,7 +404,7 @@ class Controller extends Section { }); } - changeState(state, reason) { + state(state, reason) { this.state = state; this.reason = reason; this.repaint(); diff --git a/modules/zone/back/methods/zone/toggleIsIncluded.js b/modules/zone/back/methods/zone/toggleIsIncluded.js index bf8c86f46..98c64c4a0 100644 --- a/modules/zone/back/methods/zone/toggleIsIncluded.js +++ b/modules/zone/back/methods/zone/toggleIsIncluded.js @@ -30,18 +30,21 @@ module.exports = Self => { Self.toggleIsIncluded = async(id, geoId, isIncluded, options) => { const models = Self.app.models; const myOptions = {}; - if (typeof options == 'object') Object.assign(myOptions, options); if (isIncluded === undefined) return models.ZoneIncluded.destroyAll({zoneFk: id, geoFk: geoId}, myOptions); - else { - return models.ZoneIncluded.upsert({ - zoneFk: id, - geoFk: geoId, - isIncluded: isIncluded - }, myOptions); - } + + const zoneIncluded = await models.ZoneIncluded.findOne({where: {zoneFk: id, geoFk: geoId}}, myOptions); + + if (zoneIncluded) + return zoneIncluded.updateAttribute('isIncluded', isIncluded, myOptions); + + return models.ZoneIncluded.create({ + zoneFk: id, + geoFk: geoId, + isIncluded: isIncluded + }, myOptions); }; }; diff --git a/modules/zone/back/models/zone-included.json b/modules/zone/back/models/zone-included.json index 61633a3c7..deba73f34 100644 --- a/modules/zone/back/models/zone-included.json +++ b/modules/zone/back/models/zone-included.json @@ -7,8 +7,8 @@ } }, "properties": { - "zoneFk": { - "id": true, + "id": { + "id": true, "type": "number" }, "isIncluded": { diff --git a/package-lock.json b/package-lock.json index c3f88bc2c..b66279ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "salix-back", - "version": "23.42.01", + "version": "23.48.01", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "salix-back", - "version": "23.42.01", + "version": "23.48.01", "license": "GPL-3.0", "dependencies": { "axios": "^1.2.2", diff --git a/package.json b/package.json index 3320705f5..04fcb008b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-back", - "version": "23.42.01", + "version": "23.48.01", "author": "Verdnatura Levante SL", "description": "Salix backend", "license": "GPL-3.0", diff --git a/print/templates/email/incoterms-authorization/incoterms-authorization.js b/print/templates/email/incoterms-authorization/incoterms-authorization.js index cb3cebc09..9e98f60a9 100755 --- a/print/templates/email/incoterms-authorization/incoterms-authorization.js +++ b/print/templates/email/incoterms-authorization/incoterms-authorization.js @@ -28,6 +28,10 @@ module.exports = { companyId: { type: Number, required: true + }, + addressId: { + type: Number, + required: true } } }; diff --git a/print/templates/reports/delivery-note/delivery-note.html b/print/templates/reports/delivery-note/delivery-note.html index 0be5a30f0..92dd1b126 100644 --- a/print/templates/reports/delivery-note/delivery-note.html +++ b/print/templates/reports/delivery-note/delivery-note.html @@ -117,7 +117,7 @@ {{service.price | currency('EUR', $i18n.locale)}} {{service.taxDescription}} - {{service.price | currency('EUR', $i18n.locale)}} + {{service.total | currency('EUR', $i18n.locale)}} diff --git a/print/templates/reports/delivery-note/sql/services.sql b/print/templates/reports/delivery-note/sql/services.sql index d64e8dc26..ec8a3e7ac 100644 --- a/print/templates/reports/delivery-note/sql/services.sql +++ b/print/templates/reports/delivery-note/sql/services.sql @@ -1,8 +1,9 @@ SELECT tc.code taxDescription, ts.description, - ts.quantity, - ts.price + ts.quantity, + ts.price, + ts.quantity * ts.price total FROM ticketService ts JOIN taxClass tc ON tc.id = ts.taxClassFk WHERE ts.ticketFk = ? \ No newline at end of file diff --git a/print/templates/reports/incoterms-authorization/incoterms-authorization.html b/print/templates/reports/incoterms-authorization/incoterms-authorization.html index 99a5e9caa..8c0a327ab 100644 --- a/print/templates/reports/incoterms-authorization/incoterms-authorization.html +++ b/print/templates/reports/incoterms-authorization/incoterms-authorization.html @@ -3,16 +3,16 @@

diff --git a/print/templates/reports/incoterms-authorization/incoterms-authorization.js b/print/templates/reports/incoterms-authorization/incoterms-authorization.js index 53425487e..22ae13167 100755 --- a/print/templates/reports/incoterms-authorization/incoterms-authorization.js +++ b/print/templates/reports/incoterms-authorization/incoterms-authorization.js @@ -7,6 +7,7 @@ module.exports = { this.client = await this.findOneFromDef('client', [this.id]); this.checkMainEntity(this.client); this.company = await this.findOneFromDef('company', [this.companyId]); + this.address = await this.findOneFromDef('address', [this.addressId]); }, props: { id: { @@ -17,6 +18,10 @@ module.exports = { companyId: { type: Number, required: true + }, + addressId: { + type: Number, + required: true } } }; diff --git a/print/templates/reports/incoterms-authorization/locale/es.yml b/print/templates/reports/incoterms-authorization/locale/es.yml index 6936ebda9..ef63e9e3c 100644 --- a/print/templates/reports/incoterms-authorization/locale/es.yml +++ b/print/templates/reports/incoterms-authorization/locale/es.yml @@ -1,28 +1,25 @@ reportName: autorizacion-incoterms description: '{socialName} una sociedad debidamente constituida con responsabilidad limitada -y registrada conforme al derecho de sociedades de {country} y aquí representada por -___________________. {socialName}, con domicilio en {address}, +y registrada conforme al derecho de sociedades de {country} y aquí representada por {socialName}, con domicilio en {address}, CIF {fiscalID}. En adelante denominada {name}.' issued: 'En {0}, a {1} de {2} de {3}' client: 'Cliente {0}' declaration: '{socialName} declara por la presente que:' declarations: - - 'Todas las compras realizadas por {socialName} con {companyName} se -entregan, Ex Works (Incoterms), en el almacén de {companyName} situado en -{companyCity}.' - - '{socialName} reconoce que es importante para {companyName} tener -comprobante de la entrega intracomunitaria de la mercancía a {destinationCountry} para + - 'Todas las compras realizadas por {socialName} con {companyName} se entregan según las condiciones definidas en el incoterm.' + - '{socialName} reconoce que es importante para {companyName} tener +comprobante de la entrega intracomunitaria de la mercancía a {destinationCountry} para poder facturar con 0% de IVA.' - - 'Por tanto, al firmar este acuerdo, {socialName} declara que todos los bienes que + - 'Por tanto, al firmar este acuerdo, {socialName} declara que todos los bienes que se compren a {companyName} serán entregados a {destinationCountry}.' - - 'Además, {socialName} deberá, a primera solicitud de {companyName}, -proporcionar una prueba de que todos los productos comprados a {companyName} han + - 'Además, {socialName} deberá, a primera solicitud de {companyName}, +proporcionar una prueba de que todos los productos comprados a {companyName} han sido entregados en {destinationCountry}.' - - 'Además de lo anterior, {companyName} proporcionará a {socialName} -un resumen mensual en el que se incluyen todas las facturas (y las entregas correspondientes). -{socialName} firmará y devolverá el resumen mensual a {companyName}, + - 'Además de lo anterior, {companyName} proporcionará a {socialName} +un resumen mensual en el que se incluyen todas las facturas (y las entregas correspondientes). +{socialName} firmará y devolverá el resumen mensual a {companyName}, S.L. dentro de los 5 días posteriores a la recepción del resumen.' -signer: +signer: representative: Representante representativeRole: Cargo del representante signed: Fecha de firma @@ -39,4 +36,4 @@ months: - 'Septiembre' - 'Octubre' - 'Noviembre' - - 'Diciembre' \ No newline at end of file + - 'Diciembre' diff --git a/print/templates/reports/incoterms-authorization/locale/pt.yml b/print/templates/reports/incoterms-authorization/locale/pt.yml index 2d33e6a1a..996c0961e 100644 --- a/print/templates/reports/incoterms-authorization/locale/pt.yml +++ b/print/templates/reports/incoterms-authorization/locale/pt.yml @@ -1,28 +1,27 @@ reportName: autorizacion-incoterms description: '{socialName} uma sociedade devidamente constituída com responsabilidade limitada e registada -conforme ao direito de sociedades da {country} e aqui representada por -___________________. {socialName}, com domicílio em {address}, +conforme ao direito de sociedades da {country} e aqui representada por {socialName}, com domicílio em {address}, CIF {fiscalID}. Em adiante denominada {name}.' issued: 'Em {0}, em {1} de {2} de {3}' client: 'Cliente {0}' declaration: '{socialName} declara através da presente que:' declarations: - - 'Todas as compras realizadas por {socialName} a {companyName} se entregam, + - 'Todas as compras realizadas por {socialName} a {companyName} se entregam, Ex Works (Incoterms), no armazém da {companyName} situado em {companyCity}.' - - '{socialName} reconhece ser importante para {companyName} - ter o comprovante da entrega intracomunitária da mercadoria a {destinationCountry} + - '{socialName} reconhece ser importante para {companyName} + ter o comprovante da entrega intracomunitária da mercadoria em {destinationCountry} para poder faturar com 0% de IVA.' - - 'Portanto, ao assinar este acordo, {socialName} declara que todos os bens + - 'Portanto, ao assinar este acordo, {socialName} declara que todos os bens que se comprem na {companyName} serão entregues na {destinationCountry}.' - 'Além disto, {socialName} deverá, na primeira solicitude da {companyName}, - proporcionar uma prova de que todos os produtos comprados na {companyName} - foram entregues na {destinationCountry}.' - - 'Além do anterio, {companyName} proporcionará a {socialName} + proporcionar uma prova de que todos os produtos comprados em {companyName} + foram entregues em {destinationCountry}.' + - 'Além do anterior, {companyName} proporcionará a {socialName} um resumo mensal onde se incluem todas as faturas (e as entregas correspondentes). {socialName} assinará e devolverá o resumo mensal à {companyName}, dentro dos 5 dias posteriores à receção do resumo.' -signer: +signer: representative: Representante representativeRole: Cargo de representante signed: Data da assinatura @@ -39,4 +38,4 @@ months: - 'Setembro' - 'Outubro' - 'Novembro' - - 'Dezembro' \ No newline at end of file + - 'Dezembro' diff --git a/print/templates/reports/incoterms-authorization/sql/address.sql b/print/templates/reports/incoterms-authorization/sql/address.sql new file mode 100644 index 000000000..a432c5e53 --- /dev/null +++ b/print/templates/reports/incoterms-authorization/sql/address.sql @@ -0,0 +1,9 @@ +SELECT + a.nickname, + a.street, + a.postalCode, + a.city, + p.name province +FROM address a + LEFT JOIN province p ON p.id = a.provinceFk +WHERE a.id = ? diff --git a/print/templates/reports/incoterms-authorization/sql/company.sql b/print/templates/reports/incoterms-authorization/sql/company.sql index 39c3290d1..8d94859e1 100644 --- a/print/templates/reports/incoterms-authorization/sql/company.sql +++ b/print/templates/reports/incoterms-authorization/sql/company.sql @@ -1,8 +1,8 @@ -SELECT +SELECT s.name, s.city, cl.name AS manager FROM company c JOIN supplier s ON s.id = c.id JOIN client cl ON cl.id = c.workerManagerFk -WHERE c.id = ? \ No newline at end of file +WHERE c.id = ? diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js index 1c9965d3b..b26472b08 100755 --- a/print/templates/reports/invoice/invoice.js +++ b/print/templates/reports/invoice/invoice.js @@ -7,6 +7,7 @@ module.exports = { mixins: [vnReport], async serverPrefetch() { this.invoice = await this.findOneFromDef('invoice', [this.reference]); + this.checkMainEntity(this.invoice); this.client = await this.findOneFromDef('client', [this.reference]); this.taxes = await this.rawSqlFromDef(`taxes`, [this.reference]);