Merge branch 'dev' into 3946-permisos
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2023-04-03 15:03:45 +02:00
commit 6d6ea802d0
1677 changed files with 104274 additions and 53645 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@ coverage
node_modules
dist
storage
.idea
npm-debug.log
.eslintcache
datasources.*.json

View File

@ -4,5 +4,11 @@
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"search.useIgnoreFiles": false,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}

103
CHANGELOG.md Normal file
View File

@ -0,0 +1,103 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2314.01] - 2023-04-20
### Added
-
### Changed
-
### Fixed
-
## [2312.01] - 2023-04-06
### Added
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
### Changed
- (Monitor tickets) Cuando se filtra por 'Pendiente' ya no muestra los estados de 'Previa'
- (Envíos -> Extra comunitarios) Se agrupan las entradas del mismo travel. Añadidos campos Referencia y Importe.
- (Envíos -> Índice) Cambiado el buscador superior por uno lateral
## [2310.01] - 2023-03-23
### Added
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
### Fixed
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
- (General) Al pasar el ratón por encima del icono de "Borrar" en un campo, se hacía más grande afectando a la interfaz
## [2308.01] - 2023-03-09
### Added
- (Proveedores -> Datos fiscales) Añadido checkbox 'Vies'
- (Client -> Descriptor) Nuevo icono $ con barrotes para los clientes con impago
- (Trabajador -> Datos Básicos) Añadido nuevo campo Taquilla
- (Trabajador -> PDA) Nueva sección
### Changed
- (Ticket -> Borrar ticket) Restringido el borrado de tickets con abono
## [2306.01] - 2023-02-23
### Added
- (Tickets -> Datos Básicos) Mensaje de confirmación al intentar generar tickets con negativos
- (Artículos) El visible y disponible se calcula a partir de un almacén diferente dependiendo de la sección en la que te encuentres. Se ha añadido un icono que informa sobre a partir de que almacén se esta calculando.
### Changed
- (General -> Inicio) Ahora permite recuperar la contraseña tanto con el correo de recuperación como el usuario
### Fixed
- (Monitor de tickets) Cuando ordenas por columna, ya no se queda deshabilitado el botón de 'Actualizar'
- (Zone -> Días de entrega) Al hacer click en un día, muestra correctamente las zonas
- (Artículos) El disponible en la vista previa se muestra correctamente
## [2304.01] - 2023-02-09
### Added
- (Rutas) Al descargar varias facturas se comprime en un zip
- (Trabajadores -> Nuevo trabajador) Nueva sección
- (Tickets -> Adelantar tickets) Añadidos campos "líneas" y "litros" al ticket origen
- (Tickets -> Adelantar tickets) Nuevo icono muestra cuando las agencias de los tickets origen/destino son distintas
### Changed
- (Entradas -> Compras) Cambiados los campos "Precio Grouping/Packing" por "PVP" y "Precio" por "Coste"
- (Artículos -> Últimas entradas) Cambiados los campos "P.P.U." y "P.P.P." por "PVP"
- (Rutas -> Sumario/Tickets) Actualizados campos de los tickets
- (Proveedores -> Crear/Editar) Permite añadir Proveedores con la misma razón social pero con países distintos
- (Tickets -> Adelantar tickets) Cambiados selectores de estado por checks "Pendiente origen/destino"
- (Tickets -> Adelantar tickets) Cambiado stock de destino a origen.
### Fixed
- (Artículos -> Etiquetas) Permite intercambiar la relevancia entre dos etiquetas.
- (Cliente -> Datos Fiscales) No se permite seleccionar 'Notificar vía e-mail' a los clientes sin e-mail
- (Tickets -> Datos básicos) Permite guardar la hora de envío
- (Tickets -> Añadir pago) Eliminado "null" en las referencias
- (Tickets -> Adelantar tickets) Permite ordenar por importe
- (Tickets -> Adelantar tickets) El filtrado por encajado muestra también los tickets sin tipo de encajado
## [2302.01] - 2023-01-26
### Added
- (General -> Inicio) Permite recuperar la contraseña
- (Tickets -> Opciones) Subir albarán a Docuware
- (Tickets -> Opciones) Enviar correo con PDF de Docuware
- (Artículos -> Datos Básicos) Añadido campo Unidades/Caja
### Changed
- (Reclamaciones -> Descriptor) Cambiado el campo Agencia por Zona
- (Tickets -> Líneas preparadas) Actualizada sección para que sea más visual
### Fixed
- (General) Al utilizar el traductor de Google se descuadraban los iconos
### Removed
- (Tickets -> Control clientes) Eliminada sección

View File

@ -1,32 +1,44 @@
FROM debian:stretch-slim
FROM debian:bullseye-slim
ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive
# NodeJs
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
ca-certificates \
gnupg2 \
libfontconfig lftp \
&& apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& curl -sL https://deb.nodesource.com/setup_12.x | bash - \
graphicsmagick \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2
# Puppeteer
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
nodejs \
&& apt-get purge -y --auto-remove \
gnupg2 \
libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \
libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2
# Salix
WORKDIR /salix
COPY print/package.json print/package-lock.json print/
RUN npm --prefix ./print install --omit=dev ./print
COPY package.json package-lock.json ./
COPY loopback/package.json loopback/
COPY print/package.json print/
RUN npm install --only=prod
RUN npm --prefix ./print install --only=prod ./print
RUN npm install --omit=dev
COPY loopback loopback
COPY back back

1
Jenkinsfile vendored
View File

@ -1,5 +1,4 @@
#!/usr/bin/env groovy
pipeline {
agent any
options {

View File

@ -0,0 +1,78 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('privileges', {
description: 'Change role and hasGrant if user has privileges',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'roleFk',
type: 'number',
description: 'The new role for user',
},
{
arg: 'hasGrant',
type: 'boolean',
description: 'Whether to has grant'
}
],
http: {
path: `/:id/privileges`,
verb: 'POST'
}
});
Self.privileges = async function(ctx, id, roleFk, hasGrant, options) {
if (!(hasGrant != null || roleFk)) return;
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const user = await models.Account.findById(userId, {fields: ['hasGrant']}, myOptions);
const userToUpdate = await models.Account.findById(id, {
fields: ['id', 'name', 'hasGrant', 'roleFk', 'password'],
include: {
relation: 'role',
scope: {
fields: ['name']
}
}
}, myOptions);
if (!user.hasGrant)
throw new UserError(`You don't have grant privilege`);
const hasRoleFromUser = await models.Account.hasRole(userId, userToUpdate.role().name, myOptions);
if (!hasRoleFromUser)
throw new UserError(`You don't own the role and you can't assign it to another user`);
if (hasGrant != null)
userToUpdate.hasGrant = hasGrant;
if (roleFk) {
const role = await models.Role.findById(roleFk, {fields: ['name']}, myOptions);
const hasRole = await models.Account.hasRole(userId, role.name, myOptions);
if (!hasRole)
throw new UserError(`You don't own the role and you can't assign it to another user`);
userToUpdate.roleFk = roleFk;
}
await userToUpdate.save(userToUpdate);
await models.UserAccount.sync(userToUpdate.name);
};
};

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethod('recoverPassword', {
description: 'Send email to the user',
accepts: [
{
arg: 'user',
type: 'string',
description: 'The user name or email',
required: true
}
],
http: {
path: `/recoverPassword`,
verb: 'POST'
}
});
Self.recoverPassword = async function(user) {
const models = Self.app.models;
const usesEmail = user.indexOf('@') !== -1;
if (!usesEmail) {
const account = await models.Account.findOne({
fields: ['email'],
where: {name: user}
});
user = account.email;
}
try {
await models.user.resetPassword({email: user, emailTemplate: 'recover-password'});
} catch (err) {
if (err.code === 'EMAIL_NOT_FOUND')
return;
else
throw err;
}
};
};

View File

@ -0,0 +1,115 @@
const models = require('vn-loopback/server/server').models;
describe('account privileges()', () => {
const employeeId = 1;
const developerId = 9;
const sysadminId = 66;
const itBossId = 104;
const rootId = 100;
const clarkKent = 1103;
it('should throw an error when user not has privileges', async() => {
const ctx = {req: {accessToken: {userId: developerId}}};
const tx = await models.Account.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, employeeId, null, true, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`You don't have grant privilege`);
});
it('should throw an error when user has privileges but not has the role', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, employeeId, rootId, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`You don't own the role and you can't assign it to another user`);
});
it('should throw an error when user has privileges but not has the role from user', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, itBossId, developerId, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`You don't own the role and you can't assign it to another user`);
});
it('should change role', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
const options = {transaction: tx};
const agency = await models.Role.findOne({
where: {
name: 'agency'
}
}, options);
let error;
let result;
try {
await models.Account.privileges(ctx, clarkKent, agency.id, null, options);
result = await models.Account.findById(clarkKent, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).not.toBeDefined();
expect(result.roleFk).toEqual(agency.id);
});
it('should change hasGrant', async() => {
const ctx = {req: {accessToken: {userId: sysadminId}}};
const tx = await models.Account.beginTransaction({});
let error;
let result;
try {
const options = {transaction: tx};
await models.Account.privileges(ctx, clarkKent, null, true, options);
result = await models.Account.findById(clarkKent, null, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).not.toBeDefined();
expect(result.hasGrant).toBeTruthy();
});
});

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server');
describe('account changePassword()', () => {
describe('account setPassword()', () => {
it('should throw an error when password does not meet requirements', async() => {
let req = app.models.Account.setPassword(1, 'insecurePass');

View File

@ -22,15 +22,19 @@ module.exports = Self => {
Self.latest = async filter => {
const conn = Self.dataSource.connector;
const minDate = new Date();
const minDate = Date.vnNew();
minDate.setFullYear(minDate.getFullYear() - 1);
const where = {dated: {gte: minDate}};
filter = mergeFilters(filter, {where});
const stmt = new ParameterizedSQL(
`SELECT * FROM campaign`);
`SELECT * FROM (`);
stmt.merge('SELECT * FROM campaign');
stmt.merge(conn.makeWhere(filter.where));
stmt.merge('ORDER BY dated ASC');
stmt.merge('LIMIT 10000000000000000000');
stmt.merge(') sub');
stmt.merge('GROUP BY code');
stmt.merge(conn.makePagination(filter));

View File

@ -2,7 +2,7 @@ const app = require('vn-loopback/server/server');
describe('campaign latest()', () => {
it('should return the campaigns from the last year', async() => {
const now = new Date();
const now = Date.vnNew();
const result = await app.models.Campaign.latest();
const randomIndex = Math.floor(Math.random() * result.length);
const campaignDated = result[randomIndex].dated;
@ -12,7 +12,7 @@ describe('campaign latest()', () => {
});
it('should return the campaigns from the current year', async() => {
const now = new Date();
const now = Date.vnNew();
const currentYear = now.getFullYear();
const result = await app.models.Campaign.latest({
where: {dated: {like: `%${currentYear}%`}}

View File

@ -4,7 +4,7 @@ describe('campaign upcoming()', () => {
it('should return the upcoming campaign but from the last year', async() => {
const response = await app.models.Campaign.upcoming();
const campaignDated = response.dated;
const now = new Date();
const now = Date.vnNew();
expect(campaignDated).toEqual(jasmine.any(Date));
expect(campaignDated).toBeLessThanOrEqual(now);

View File

@ -14,7 +14,7 @@ module.exports = Self => {
});
Self.upcoming = async() => {
const minDate = new Date();
const minDate = Date.vnNew();
minDate.setFullYear(minDate.getFullYear() - 1);
return Self.findOne({

View File

@ -21,7 +21,7 @@ module.exports = Self => {
if (!this.login) return;
if (Date.now() > this.login.expires)
if (Date.vnNow() > this.login.expires)
this.login = await requestToken();
return this.login;
@ -48,7 +48,7 @@ module.exports = Self => {
userId: requestData.userId,
token: requestData.authToken
},
expires: Date.now() + (1000 * 60 * tokenLifespan)
expires: Date.vnNow() + (1000 * 60 * tokenLifespan)
};
}
}

View File

@ -8,7 +8,7 @@ module.exports = Self => {
},
http: {
path: `/notifyIssues`,
verb: 'GET'
verb: 'POST'
}
});
@ -32,7 +32,7 @@ module.exports = Self => {
let message = $t(`There's a new urgent ticket:`);
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
tickets.forEach(ticket => {
message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} (@${ticket.username})](${ostUri + ticket.id})`;
message += `\r\n[ID: ${ticket.number} - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`;
});
const department = await models.Department.findOne({
@ -42,7 +42,5 @@ module.exports = Self => {
if (channelName)
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
return;
};
};

View File

@ -30,16 +30,23 @@ module.exports = Self => {
const recipient = to.replace('@', '');
if (sender.name != recipient) {
await models.Chat.create({
const chat = await models.Chat.create({
senderFk: sender.id,
recipient: to,
dated: new Date(),
dated: Date.vnNew(),
checkUserStatus: 0,
message: message,
status: 0,
status: 'sending',
attempts: 0
});
try {
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
await Self.updateChat(chat, 'sent');
} catch (error) {
await Self.updateChat(chat, 'error', error);
}
return true;
}

View File

@ -24,18 +24,13 @@ module.exports = Self => {
}
});
Self.sendCheckingPresence = async(ctx, recipientId, message, options) => {
Self.sendCheckingPresence = async(ctx, recipientId, message) => {
if (!recipientId) return false;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const sender = await models.Account.findById(userId);
const recipient = await models.Account.findById(recipientId, null, myOptions);
const sender = await models.Account.findById(userId, {fields: ['id']});
const recipient = await models.Account.findById(recipientId, null);
// Prevent sending messages to yourself
if (recipientId == userId) return false;
@ -43,16 +38,26 @@ module.exports = Self => {
if (!recipient)
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
await models.Chat.create({
if (process.env.NODE_ENV == 'test')
message = `[Test:Environment to user ${userId}] ` + message;
const chat = await models.Chat.create({
senderFk: sender.id,
recipient: `@${recipient.name}`,
dated: new Date(),
dated: Date.vnNew(),
checkUserStatus: 1,
message: message,
status: 0,
status: 'sending',
attempts: 0
});
try {
await Self.sendCheckingUserStatus(chat);
await Self.updateChat(chat, 'sent');
} catch (error) {
await Self.updateChat(chat, 'error', error);
}
return true;
};
};

View File

@ -3,7 +3,6 @@ module.exports = Self => {
Self.remoteMethodCtx('sendQueued', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [],
returns: {
type: 'object',
root: true
@ -16,14 +15,17 @@ module.exports = Self => {
Self.sendQueued = async() => {
const models = Self.app.models;
const maxAttempts = 3;
const sentStatus = 1;
const errorStatus = 2;
const chats = await models.Chat.find({
where: {
status: {neq: sentStatus},
attempts: {lt: maxAttempts}
status: {
nin: [
'sent',
'sending'
]
},
attempts: {lt: 3}
}
});
@ -31,16 +33,16 @@ module.exports = Self => {
if (chat.checkUserStatus) {
try {
await Self.sendCheckingUserStatus(chat);
await updateChat(chat, sentStatus);
await Self.updateChat(chat, 'sent');
} catch (error) {
await updateChat(chat, errorStatus);
await Self.updateChat(chat, 'error', error);
}
} else {
try {
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
await updateChat(chat, sentStatus);
await Self.updateChat(chat, 'sent');
} catch (error) {
await updateChat(chat, errorStatus);
await Self.updateChat(chat, 'error', error);
}
}
}
@ -75,7 +77,7 @@ module.exports = Self => {
const channelName = department && department.chatName;
if (channelName)
return Self.sendMessage(chat.senderFk, `#${channelName}`, `@${recipient.name}${message}`);
return Self.sendMessage(chat.senderFk, `#${channelName}`, `@${recipient.name}${chat.message}`);
else
return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message);
} else
@ -127,14 +129,18 @@ module.exports = Self => {
*
* @param {object} chat - The chat
* @param {string} status - The new status
* @param {string} error - The error
* @param {object} options - Query options
* @return {Promise} - The request promise
*/
async function updateChat(chat, status) {
Self.updateChat = async(chat, status, error) => {
return chat.updateAttributes({
status: status,
attempts: ++chat.attempts
attempts: ++chat.attempts,
error: error
});
}
};
/**
* Returns the current user status on Rocketchat

View File

@ -27,7 +27,7 @@ describe('Chat notifyIssue()', () => {
subject: 'Issue title'}
]);
// eslint-disable-next-line max-len
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: 00001 - Issue title @batman](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName;

View File

@ -1,7 +1,7 @@
const models = require('vn-loopback/server/server').models;
describe('Chat sendCheckingPresence()', () => {
const today = new Date();
const today = Date.vnNew();
today.setHours(6, 0);
const chatModel = models.Chat;
@ -10,7 +10,7 @@ describe('Chat sendCheckingPresence()', () => {
const chat = {
checkUserStatus: 1,
status: 0,
status: 'pending',
attempts: 0
};
@ -27,7 +27,7 @@ describe('Chat sendCheckingPresence()', () => {
const chat = {
checkUserStatus: 0,
status: 0,
status: 'pending',
attempts: 0
};

View File

@ -0,0 +1,35 @@
module.exports = Self => {
Self.remoteMethodCtx('previousLabel', {
description: 'Returns the previa label pdf',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The item id',
http: {source: 'path'}
}],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/previousLabel',
verb: 'GET'
}
});
Self.previousLabel = (ctx, id) => Self.printReport(ctx, id, 'previa-label');
};

View File

@ -24,13 +24,32 @@ module.exports = Self => {
}
});
Self.setSaleQuantity = async(saleId, quantity) => {
Self.setSaleQuantity = async(saleId, quantity, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
const sale = await models.Sale.findById(saleId);
return await sale.updateAttributes({
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const sale = await models.Sale.findById(saleId, null, myOptions);
const saleUpdated = await sale.updateAttributes({
originalQuantity: sale.quantity,
quantity: quantity
});
}, myOptions);
if (tx) await tx.commit();
return saleUpdated;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -2,10 +2,10 @@ const models = require('vn-loopback/server/server').models;
describe('ticket getCollection()', () => {
it('should return a list of collections', async() => {
let ctx = {req: {accessToken: {userId: 1106}}};
let ctx = {req: {accessToken: {userId: 1107}}};
let response = await models.Collection.getCollection(ctx);
expect(response.length).toBeGreaterThan(0);
expect(response[0].collectionFk).toEqual(1);
expect(response[0].collectionFk).toEqual(3);
});
});

View File

@ -1,16 +1,42 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('setSaleQuantity()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const saleId = 30;
const newQuantity = 10;
const originalSale = await models.Sale.findById(saleId);
const originalSale = await models.Sale.findById(saleId, null, options);
await models.Collection.setSaleQuantity(saleId, newQuantity);
const updateSale = await models.Sale.findById(saleId);
await models.Collection.setSaleQuantity(saleId, newQuantity, options);
const updateSale = await models.Sale.findById(saleId, null, options);
expect(updateSale.originalQuantity).toEqual(originalSale.quantity);
expect(updateSale.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -1,3 +1,4 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
@ -15,17 +16,15 @@ module.exports = Self => {
}
});
Self.deleteTrashFiles = async(options) => {
const tx = await Self.beginTransaction({});
Self.deleteTrashFiles = async options => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction)
myOptions.transaction = tx;
if (process.env.NODE_ENV == 'test')
throw new UserError(`Action not allowed on the test environment`);
try {
const models = Self.app.models;
const DmsContainer = models.DmsContainer;
@ -33,25 +32,37 @@ module.exports = Self => {
where: {code: 'trash'}
}, myOptions);
const date = Date.vnNew();
date.setMonth(date.getMonth() - 4);
const dmsToDelete = await models.Dms.find({
where: {
dmsTypeFk: trashDmsType.id
and: [
{dmsTypeFk: trashDmsType.id},
{created: {lt: date}}
]
}
}, myOptions);
for (let dms of dmsToDelete) {
const pathHash = DmsContainer.getHash(dms.id);
const dmsContainer = await DmsContainer.container(pathHash);
try {
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
await fs.unlink(dstFile);
await dms.destroy(myOptions);
} catch (err) {
if (err.code != 'ENOENT' && dms.file)
throw err;
}
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
await dms.destroy(myOptions);
throw e;
const dstFolder = path.join(dmsContainer.client.root, pathHash);
try {
await fs.rmdir(dstFolder);
} catch (err) {
continue;
}
}
};
};

View File

@ -1,4 +1,4 @@
const got = require('got');
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('checkFile', {
@ -18,14 +18,14 @@ module.exports = Self => {
description: 'The fileCabinet name'
},
{
arg: 'dialog',
type: 'string',
arg: 'signed',
type: 'boolean',
required: true,
description: 'The dialog name'
description: 'If pdf is necessary to be signed'
}
],
returns: {
type: 'boolean',
type: 'object',
root: true
},
http: {
@ -34,58 +34,51 @@ module.exports = Self => {
}
});
Self.checkFile = async function(ctx, id, fileCabinet, dialog) {
const myUserId = ctx.req.accessToken.userId;
if (!myUserId)
return false;
Self.checkFile = async function(ctx, id, fileCabinet, signed) {
const models = Self.app.models;
const docuwareConfig = await models.DocuwareConfig.findOne();
const action = 'find';
const docuwareInfo = await models.Docuware.findOne({
where: {
code: fileCabinet,
dialogName: dialog
action: action
}
});
const docuwareUrl = docuwareConfig.url;
const cookie = docuwareConfig.token;
const fileCabinetName = docuwareInfo.fileCabinetName;
const find = docuwareInfo.find;
const options = {
'headers': {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': cookie
}
};
const searchFilter = {
condition: [
{
DBName: find,
DBName: docuwareInfo.findById,
Value: [id]
}
],
sortOrder: [
{
Field: 'FILENAME',
Direction: 'Desc'
}
]
};
try {
// get fileCabinetId
const fileCabinetResponse = await got.get(`${docuwareUrl}/FileCabinets`, options);
const fileCabinetJson = JSON.parse(fileCabinetResponse.body).FileCabinet;
const fileCabinetId = fileCabinetJson.find(dialogs => dialogs.Name === fileCabinetName).Id;
const options = await Self.getOptions();
// get dialog
const dialogResponse = await got.get(`${docuwareUrl}/FileCabinets/${fileCabinetId}/dialogs`, options);
const dialogJson = JSON.parse(dialogResponse.body).Dialog;
const dialogId = dialogJson.find(dialogs => dialogs.DisplayName === 'find').Id;
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
// get docuwareID
Object.assign(options, {'body': JSON.stringify(searchFilter)});
const response = await got.post(
`${docuwareUrl}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`, options);
JSON.parse(response.body).Items[0].Id;
const response = await axios.post(
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
searchFilter,
options.headers
);
const [documents] = response.data.Items;
if (!documents) return false;
return true;
const state = documents.Fields.find(field => field.FieldName == 'ESTADO');
if (signed && state.Item != 'Firmado') return false;
return {id: documents.Id};
} catch (error) {
return false;
}

View File

@ -0,0 +1,78 @@
const axios = require('axios');
module.exports = Self => {
/**
* Returns the dialog id
*
* @param {string} code - The fileCabinet name
* @param {string} action - The fileCabinet name
* @param {string} fileCabinetId - Optional The fileCabinet name
* @return {number} - The fileCabinet id
*/
Self.getDialog = async(code, action, fileCabinetId) => {
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code: code,
action: action
}
});
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
const options = await Self.getOptions();
if (!process.env.NODE_ENV)
return Math.round();
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
const dialogs = response.data.Dialog;
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
return dialogId;
};
/**
* Returns the fileCabinetId
*
* @param {string} code - The fileCabinet code
* @return {number} - The fileCabinet id
*/
Self.getFileCabinet = async code => {
const options = await Self.getOptions();
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code: code
}
});
if (!process.env.NODE_ENV)
return Math.round();
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
const fileCabinets = fileCabinetResponse.data.FileCabinet;
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
return fileCabinetId;
};
/**
* Returns basic headers
*
* @param {string} cookie - The docuware cookie
* @return {object} - The headers
*/
Self.getOptions = async() => {
const docuwareConfig = await Self.app.models.DocuwareConfig.findOne();
const headers = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': docuwareConfig.cookie
}
};
return {
url: docuwareConfig.url,
headers
};
};
};

View File

@ -0,0 +1,72 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('deliveryNoteEmail', {
description: 'Sends the delivery note email with an docuware attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'string',
required: true,
description: 'The ticket id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
},
{
arg: 'recipientId',
type: 'number',
description: 'The client id',
required: false
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/delivery-note-email',
verb: 'POST'
}
});
Self.deliveryNoteEmail = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('delivery-note', params);
const docuwareFile = await Self.app.models.Docuware.download(ctx, id, 'deliveryNote');
return email.send({
overrideAttachments: true,
attachments: [{
filename: `${id}.pdf`,
content: docuwareFile[0]
}]
});
};
};

View File

@ -1,5 +1,5 @@
/* eslint max-len: ["error", { "code": 180 }]*/
const got = require('got');
const axios = require('axios');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
@ -10,19 +10,13 @@ module.exports = Self => {
{
arg: 'id',
type: 'number',
description: 'The id',
description: 'The ticket id',
http: {source: 'path'}
},
{
arg: 'fileCabinet',
type: 'string',
description: 'The id',
http: {source: 'path'}
},
{
arg: 'dialog',
type: 'string',
description: 'The id',
description: 'The file cabinet',
http: {source: 'path'}
}
],
@ -42,79 +36,26 @@ module.exports = Self => {
}
],
http: {
path: `/:id/download/:fileCabinet/:dialog`,
path: `/:id/download/:fileCabinet`,
verb: 'GET'
}
});
Self.download = async function(ctx, id, fileCabinet, dialog) {
const myUserId = ctx.req.accessToken.userId;
if (!myUserId)
throw new UserError(`You don't have enough privileges`);
Self.download = async function(ctx, id, fileCabinet) {
const models = Self.app.models;
const docuwareConfig = await models.DocuwareConfig.findOne();
const docuwareInfo = await models.Docuware.findOne({
where: {
code: fileCabinet,
dialogName: dialog
}
});
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, true);
if (!docuwareFile) throw new UserError('The DOCUWARE PDF document does not exists');
const docuwareUrl = docuwareConfig.url;
const cookie = docuwareConfig.token;
const fileCabinetName = docuwareInfo.fileCabinetName;
const find = docuwareInfo.find;
const options = {
'headers': {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': cookie
}
};
const searchFilter = {
condition: [
{
DBName: find,
Value: [id]
}
]
};
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const options = await Self.getOptions();
options.headers.responseType = 'stream';
try {
// get fileCabinetId
const fileCabinetResponse = await got.get(`${docuwareUrl}/FileCabinets`, options);
const fileCabinetJson = JSON.parse(fileCabinetResponse.body).FileCabinet;
const fileCabinetId = fileCabinetJson.find(dialogs => dialogs.Name === fileCabinetName).Id;
// get dialog
const dialogResponse = await got.get(`${docuwareUrl}/FileCabinets/${fileCabinetId}/dialogs`, options);
const dialogJson = JSON.parse(dialogResponse.body).Dialog;
const dialogId = dialogJson.find(dialogs => dialogs.DisplayName === 'find').Id;
// get docuwareID
Object.assign(options, {'body': JSON.stringify(searchFilter)});
const response = await got.post(`${docuwareUrl}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`, options);
const docuwareId = JSON.parse(response.body).Items[0].Id;
// download & save file
const fileName = `filename="${id}.pdf"`;
const contentType = 'application/pdf';
const downloadUri = `${docuwareUrl}/FileCabinets/${fileCabinetId}/Documents/${docuwareId}/FileDownload?targetFileType=Auto&keepAnnotations=false`;
const downloadOptions = {
'headers': {
'Cookie': cookie
}
};
const downloadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/FileDownload?targetFileType=Auto&keepAnnotations=false`;
const stream = got.stream(downloadUri, downloadOptions);
const stream = await axios.get(downloadUri, options.headers);
return [stream, contentType, fileName];
} catch (error) {
if (error.code === 'ENOENT')
throw new UserError('The DOCUWARE PDF document does not exists');
throw error;
}
return [stream.data, contentType, fileName];
};
};

View File

@ -1,5 +1,5 @@
const models = require('vn-loopback/server/server').models;
const got = require('got');
const axios = require('axios');
describe('docuware download()', () => {
const ticketId = 1;
@ -12,53 +12,71 @@ describe('docuware download()', () => {
}
};
const fileCabinetName = 'deliveryClient';
const dialogDisplayName = 'find';
const dialogName = 'findTicket';
const docuwareModel = models.Docuware;
const fileCabinetName = 'deliveryNote';
const gotGetResponse = {
body: JSON.stringify(
{
FileCabinet: [
{Id: 12, Name: fileCabinetName}
],
Dialog: [
{Id: 34, DisplayName: dialogDisplayName}
]
})
};
it('should return exist file in docuware', async() => {
const gotPostResponse = {
body: JSON.stringify(
{
Items: [
{Id: 56}
],
})
};
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, dialogName);
expect(result).toEqual(true);
beforeAll(() => {
spyOn(docuwareModel, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(docuwareModel, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
});
it('should return not exist file in docuware', async() => {
const gotPostResponse = {
body: JSON.stringify(
{
Items: [],
})
it('should return false if there are no documents', async() => {
const response = {
data: {
Items: []
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, dialogName);
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
expect(result).toEqual(false);
});
it('should return false if the document is unsigned', async() => {
const response = {
data: {
Items: [
{
Id: 1,
Fields: [
{
FieldName: 'ESTADO',
Item: 'Unsigned'
}
]
}
]
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
expect(result).toEqual(false);
});
it('should return the document data', async() => {
const docuwareId = 1;
const response = {
data: {
Items: [
{
Id: docuwareId,
Fields: [
{
FieldName: 'ESTADO',
Item: 'Firmado'
}
]
}
]
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
expect(result.id).toEqual(docuwareId);
});
});

View File

@ -1,5 +1,5 @@
const models = require('vn-loopback/server/server').models;
const got = require('got');
const axios = require('axios');
const stream = require('stream');
describe('docuware download()', () => {
@ -13,36 +13,33 @@ describe('docuware download()', () => {
}
};
it('should return the downloaded file name', async() => {
const fileCabinetName = 'deliveryClient';
const dialogDisplayName = 'find';
const dialogName = 'findTicket';
const gotGetResponse = {
body: JSON.stringify(
{
FileCabinet: [
{Id: 12, Name: fileCabinetName}
],
Dialog: [
{Id: 34, DisplayName: dialogDisplayName}
]
})
};
const docuwareModel = models.Docuware;
const fileCabinetName = 'deliveryNote';
const gotPostResponse = {
body: JSON.stringify(
{
Items: [
{Id: 56}
],
})
};
beforeAll(() => {
spyOn(docuwareModel, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(docuwareModel, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
});
spyOn(got, 'get').and.returnValue(new Promise(resolve => resolve(gotGetResponse)));
spyOn(got, 'post').and.returnValue(new Promise(resolve => resolve(gotPostResponse)));
spyOn(got, 'stream').and.returnValue(new stream.PassThrough({objectMode: true}));
it('should return error if file not exist', async() => {
spyOn(docuwareModel, 'checkFile').and.returnValue(false);
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
const result = await models.Docuware.download(ctx, ticketId, fileCabinetName, dialogName);
let error;
try {
await models.Docuware.download(ctx, ticketId, fileCabinetName);
} catch (e) {
error = e.message;
}
expect(error).toEqual('The DOCUWARE PDF document does not exists');
});
it('should return the downloaded file if exist file ', async() => {
spyOn(docuwareModel, 'checkFile').and.returnValue({});
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
const result = await models.Docuware.download(ctx, ticketId, fileCabinetName);
expect(result[1]).toEqual('application/pdf');
expect(result[2]).toEqual(`filename="${ticketId}.pdf"`);

View File

@ -0,0 +1,37 @@
const models = require('vn-loopback/server/server').models;
describe('docuware upload()', () => {
const userId = 9;
const ticketId = 10;
const ctx = {
req: {
getLocale: () => {
return 'en';
},
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const docuwareModel = models.Docuware;
const ticketModel = models.Ticket;
const fileCabinetName = 'deliveryNote';
beforeAll(() => {
spyOn(docuwareModel, 'getFileCabinet').and.returnValue(new Promise(resolve => resolve(Math.random())));
spyOn(docuwareModel, 'getDialog').and.returnValue(new Promise(resolve => resolve(Math.random())));
});
it('should try upload file', async() => {
spyOn(ticketModel, 'deliveryNotePdf').and.returnValue(new Promise(resolve => resolve({})));
let error;
try {
await models.Docuware.upload(ctx, ticketId, fileCabinetName);
} catch (e) {
error = e.message;
}
expect(error).toEqual('Action not allowed on the test environment');
});
});

View File

@ -0,0 +1,141 @@
const UserError = require('vn-loopback/util/user-error');
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('upload', {
description: 'Upload an docuware PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
description: 'The ticket id',
http: {source: 'path'}
},
{
arg: 'fileCabinet',
type: 'string',
description: 'The file cabinet'
},
{
arg: 'dialog',
type: 'string',
description: 'The dialog'
}
],
returns: [],
http: {
path: `/:id/upload`,
verb: 'POST'
}
});
Self.upload = async function(ctx, id, fileCabinet) {
const models = Self.app.models;
const action = 'store';
const options = await Self.getOptions();
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
// get delivery note
const deliveryNote = await models.Ticket.deliveryNotePdf(ctx, {
id,
type: 'deliveryNote'
});
// get ticket data
const ticket = await models.Ticket.findById(id, {
include: [{
relation: 'client',
scope: {
fields: ['id', 'socialName', 'fi']
}
}]
});
// upload file
const templateJson = {
'Fields': [
{
'FieldName': 'N__ALBAR_N',
'ItemElementName': 'string',
'Item': id,
},
{
'FieldName': 'CIF_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().fi,
},
{
'FieldName': 'CODIGO_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().id,
},
{
'FieldName': 'NOMBRE_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().socialName,
},
{
'FieldName': 'FECHA_FACTURA',
'ItemElementName': 'date',
'Item': ticket.shipped,
},
{
'FieldName': 'TOTAL_FACTURA',
'ItemElementName': 'Decimal',
'Item': ticket.totalWithVat,
},
{
'FieldName': 'ESTADO',
'ItemElementName': 'string',
'Item': 'Pendiente procesar',
},
{
'FieldName': 'FIRMA_',
'ItemElementName': 'string',
'Item': 'Si',
},
{
'FieldName': 'FILTRO_TABLET',
'ItemElementName': 'string',
'Item': 'Tablet1',
}
]
};
if (process.env.NODE_ENV != 'production')
throw new UserError('Action not allowed on the test environment');
// delete old
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, false);
if (docuwareFile) {
const deleteJson = {
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
};
const deleteUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`;
await axios.put(deleteUri, deleteJson, options.headers);
}
const uploadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`;
const FormData = require('form-data');
const data = new FormData();
data.append('document', JSON.stringify(templateJson), 'schema.json');
data.append('file[]', deliveryNote[0], 'file.pdf');
const uploadOptions = {
headers: {
'Content-Type': 'multipart/form-data',
'X-File-ModifiedDate': Date.vnNew(),
'Cookie': options.headers.headers.Cookie,
...data.getHeaders()
},
};
return await axios.post(uploadUri, data, uploadOptions)
.catch(() => {
throw new UserError('Failed to upload file');
});
};
};

View File

@ -12,7 +12,7 @@ module.exports = Self => {
},
http: {
path: `/updateData`,
verb: 'GET'
verb: 'POST'
}
});
@ -20,7 +20,11 @@ module.exports = Self => {
const models = Self.app.models;
// Get files checksum
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig');
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
const updatableFiles = [];
for (const file of files) {
@ -51,13 +55,11 @@ module.exports = Self => {
const tables = await Self.rawSql(`
SELECT fileName, toTable, file
FROM edi.tableConfig
WHERE file IN (?)`, [fileNames]);
WHERE file IN (?)`, [fileNames], options);
for (const table of tables) {
const fileName = table.file;
console.debug(`Downloading file ${fileName}...`);
remoteFile = `codes/${fileName}.ZIP`;
tempDir = `${tempPath}/${fileName}`;
tempFile = `${tempPath}/${fileName}.zip`;
@ -66,30 +68,34 @@ module.exports = Self => {
await fs.readFile(tempFile);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`Downloading file ${fileName}...`);
const downloadOutput = await downloadFile(remoteFile, tempFile);
if (downloadOutput.error)
continue;
}
}
console.debug(`Extracting file ${fileName}...`);
await extractFile(tempFile, tempDir);
await extractFile(fileName, tempFile, tempDir);
console.debug(`Updating table ${table.toTable}...`);
await dumpData(tempDir, table);
await dumpData(tempDir, table, options);
}
// Update files checksum
for (const file of updatableFiles) {
console.log(`Updating file ${file.name} checksum...`);
await Self.rawSql(`
UPDATE edi.fileConfig
SET checksum = ?
WHERE name = ?`,
[file.checksum, file.name]);
[file.checksum, file.name], options);
}
await tx.commit();
// Clean files
try {
console.debug(`Cleaning files...`);
await fs.remove(tempPath);
} catch (error) {
if (error.code !== 'ENOENT')
@ -97,6 +103,10 @@ module.exports = Self => {
}
return true;
} catch (error) {
await tx.rollback();
throw error;
}
};
let ftpClient;
@ -126,9 +136,9 @@ module.exports = Self => {
const response = await new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (response.error) {
if (err || response.error) {
console.debug(`Error downloading checksum file... ${response.error}`);
reject(err);
return reject(err);
}
resolve(response);
@ -159,9 +169,9 @@ module.exports = Self => {
return new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (response.error) {
if (err || response.error) {
console.debug(`Error downloading file... ${response.error}`);
reject(err);
return reject(err);
}
resolve(response);
@ -169,11 +179,12 @@ module.exports = Self => {
});
}
async function extractFile(tempFile, tempDir) {
async function extractFile(fileName, tempFile, tempDir) {
const JSZip = require('jszip');
try {
await fs.mkdir(tempDir);
console.debug(`Extracting file ${fileName}...`);
} catch (error) {
if (error.code !== 'EEXIST')
throw e;
@ -196,17 +207,13 @@ module.exports = Self => {
}
}
async function dumpData(tempDir, table) {
async function dumpData(tempDir, table, options) {
const toTable = table.toTable;
const baseName = table.fileName;
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
console.log(`Emptying table ${toTable}...`);
const tableName = `edi.${toTable}`;
await Self.rawSql(`DELETE FROM ??`, [tableName], options);
await Self.rawSql(`DELETE FROM ??`, [tableName]);
const dirFiles = await fs.readdir(tempDir);
const files = dirFiles.filter(file => file.startsWith(baseName));
@ -223,14 +230,9 @@ module.exports = Self => {
UPDATE edi.tableConfig
SET updated = ?
WHERE fileName = ?
`, [new Date(), baseName], options);
`, [Date.vnNew(), baseName], options);
}
tx.commit();
} catch (error) {
tx.rollback();
throw error;
}
console.log(`Updated table ${toTable}\n`);
}
};

View File

@ -74,10 +74,13 @@ module.exports = Self => {
const container = await models.ImageContainer.getContainer(collection);
const rootPath = container.client.root;
const fileSrc = path.join(rootPath, collection, size);
const ext = image.name.substring((image.name.length - 4));
const fileName = ext !== '.png' ? `${image.name}.png` : image.name;
const file = {
path: `${fileSrc}/${image.name}.png`,
path: `${fileSrc}/${fileName}`,
contentType: 'image/png',
name: `${image.name}.png`
name: image.name
};
if (!fs.existsSync(file.path)) return [];

View File

@ -1,6 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
const uuid = require('uuid');
module.exports = Self => {
Self.remoteMethodCtx('upload', {
@ -18,12 +19,6 @@ module.exports = Self => {
type: 'string',
description: 'The collection name',
required: true
},
{
arg: 'fileName',
type: 'string',
description: 'The file name',
required: true
}],
returns: {
type: 'Object',
@ -56,10 +51,12 @@ module.exports = Self => {
const [uploadedFile] = Object.values(uploaded.files).map(file => {
return file[0];
});
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await models.Image.registerImage(args.collection, srcFile, args.fileName, args.id);
const fileName = `${uuid.v4()}.png`;
await models.Image.registerImage(args.collection, srcFile, fileName, args.id);
} catch (e) {
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);

View File

@ -0,0 +1,49 @@
module.exports = Self => {
Self.remoteMethod('clean', {
description: 'clean notifications from queue',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
http: {
path: `/clean`,
verb: 'POST'
}
});
Self.clean = async options => {
const models = Self.app.models;
const status = ['sent', 'error'];
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const config = await models.NotificationConfig.findOne({}, myOptions);
if (!config.cleanDays) return;
const cleanDate = Date.vnNew();
cleanDate.setDate(cleanDate.getDate() - config.cleanDays);
await models.NotificationQueue.destroyAll({
where: {status: {inq: status}},
created: {lt: cleanDate}
}, myOptions);
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
if (tx) await tx.commit();
};
};

View File

@ -0,0 +1,83 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethod('send', {
description: 'Send notifications from queue',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
http: {
path: `/send`,
verb: 'POST'
}
});
Self.send = async options => {
const models = Self.app.models;
const findStatus = 'pending';
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const notificationQueue = await models.NotificationQueue.find({
where: {status: findStatus},
include: [
{
relation: 'notification',
scope: {
include: {
relation: 'subscription',
scope: {
include: {
relation: 'user',
scope: {
fields: ['name', 'lang'],
include: {
relation: 'emailUser'
}
}
}
}
}
}
}
]
}, myOptions);
const statusSent = 'sent';
const statusError = 'error';
for (const queue of notificationQueue) {
const queueName = queue.notification().name;
const queueParams = JSON.parse(queue.params);
for (const notificationUser of queue.notification().subscription()) {
try {
const sendParams = {
recipient: notificationUser.user().emailUser().email,
lang: notificationUser.user().lang
};
if (notificationUser.userFk == queue.authorFk) {
await queue.updateAttribute('status', statusSent);
continue;
}
const newParams = Object.assign({}, queueParams, sendParams);
const email = new Email(queueName, newParams);
if (process.env.NODE_ENV != 'test')
await email.send();
await queue.updateAttribute('status', statusSent);
} catch (error) {
await queue.updateAttribute('status', statusError);
}
}
}
};
};

View File

@ -0,0 +1,42 @@
const models = require('vn-loopback/server/server').models;
describe('Notification Clean()', () => {
it('should delete old rows with error', async() => {
const userId = 9;
const status = 'error';
const tx = await models.NotificationQueue.beginTransaction({});
const options = {transaction: tx};
const notification = await models.Notification.findOne({}, options);
const notificationConfig = await models.NotificationConfig.findOne({});
const cleanDate = Date.vnNew();
cleanDate.setDate(cleanDate.getDate() - (notificationConfig.cleanDays + 1));
let before;
let after;
try {
const notificationDelete = await models.NotificationQueue.create({
notificationFk: notification.name,
authorFk: userId,
status: status,
created: cleanDate
}, options);
before = await models.NotificationQueue.findById(notificationDelete.id, null, options);
await models.Notification.clean(options);
after = await models.NotificationQueue.findById(notificationDelete.id, null, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(before.notificationFk).toEqual(notification.name);
expect(after).toBe(null);
});
});

View File

@ -0,0 +1,33 @@
const models = require('vn-loopback/server/server').models;
describe('Notification Send()', () => {
it('should send notification', async() => {
const statusPending = 'pending';
const tx = await models.NotificationQueue.beginTransaction({});
const options = {transaction: tx};
const filter = {
where: {
status: statusPending
}
};
let before;
let after;
try {
before = await models.NotificationQueue.find(filter, options);
await models.Notification.send(options);
after = await models.NotificationQueue.find(filter, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(before.length).toEqual(3);
expect(after.length).toEqual(0);
});
});

View File

@ -0,0 +1,155 @@
const jsdom = require('jsdom');
const mysql = require('mysql');
const FormData = require('form-data');
module.exports = Self => {
Self.remoteMethodCtx('closeTicket', {
description: 'Close tickets without response from the user',
accessType: 'READ',
returns: {
type: 'object',
root: true
},
http: {
path: `/closeTicket`,
verb: 'POST'
}
});
Self.closeTicket = async ctx => {
const models = Self.app.models;
const config = await models.OsTicketConfig.findOne();
const ostUri = `${config.host}/login.php`;
if (!config.user || !config.password || !config.userDb || !config.passwordDb)
return false;
const con = mysql.createConnection({
host: config.hostDb,
user: config.userDb,
password: config.passwordDb,
port: config.portDb
});
const sql = `SELECT ot.ticket_id, ot.number
FROM osticket.ost_ticket ot
JOIN osticket.ost_ticket_status ots ON ots.id = ot.status_id
JOIN osticket.ost_thread ot2 ON ot2.object_id = ot.ticket_id AND ot2.object_type = 'T'
JOIN (
SELECT sub2.thread_id, sub2.type, sub2.updated, sub2.created
FROM (
SELECT ote.thread_id, ote.created, ote.updated, ote.type
FROM osticket.ost_thread_entry ote
WHERE ote.staff_id
ORDER BY ote.id DESC
LIMIT 10000000000000000000) sub2
GROUP BY sub2.thread_id
) sub ON sub.thread_id = ot2.id
WHERE ot.isanswered
AND ots.id IN (?)
AND sub.type = 'R'
AND IF(sub.updated > sub.created, sub.updated, sub.created) < DATE_SUB(CURDATE(), INTERVAL ? DAY);`;
const ticketsId = [];
const statusIdToClose = config.oldStatus.split(',');
con.connect(err => {
if (err) throw err;
con.query(sql, [statusIdToClose, config.day],
(err, results) => {
if (err) throw err;
for (const result of results)
ticketsId.push(result.ticket_id);
});
});
await getRequestToken();
async function getRequestToken() {
const response = await fetch(ostUri);
const result = response.headers.get('set-cookie');
const [firtHeader] = result.split(' ');
const cookie = firtHeader.substring(0, firtHeader.length - 1);
const body = await response.text();
const dom = new jsdom.JSDOM(body);
const token = dom.window.document.querySelector('[name="__CSRFToken__"]').value;
await login(token, cookie);
}
async function login(token, cookie) {
const data = {
__CSRFToken__: token,
do: 'scplogin',
userid: config.user,
passwd: config.password,
ajax: 1
};
const params = {
method: 'POST',
body: new URLSearchParams(data),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Cookie': cookie
}
};
await fetch(ostUri, params);
await close(token, cookie);
}
async function close(token, cookie) {
for (const ticketId of ticketsId) {
try {
const lock = await getLockCode(token, cookie, ticketId);
if (!lock.code) {
let error = `Can't get lock code`;
if (lock.msg) error += `: ${lock.msg}`;
throw new Error(error);
}
let form = new FormData();
form.append('__CSRFToken__', token);
form.append('id', ticketId);
form.append('a', config.responseType);
form.append('lockCode', lock.code);
form.append('from_email_id', config.fromEmailId);
form.append('reply-to', config.replyTo);
form.append('cannedResp', 0);
form.append('response', config.comment);
form.append('signature', 'none');
form.append('reply_status_id', config.newStatusId);
const ostUri = `${config.host}/tickets.php?id=${ticketId}`;
const params = {
method: 'POST',
body: form,
headers: {
'Cookie': cookie
}
};
await fetch(ostUri, params);
} catch (e) {
const err = new Error(`${ticketId} Ticket close failed: ${e.message}`);
err.stack += e.stack;
console.error(err);
}
}
}
async function getLockCode(token, cookie, ticketId) {
const ostUri = `${config.host}/ajax.php/lock/ticket/${ticketId}`;
const params = {
method: 'POST',
headers: {
'X-CSRFToken': token,
'Cookie': cookie
}
};
const response = await fetch(ostUri, params);
const body = await response.text();
const json = JSON.parse(body);
return json;
}
};
};

View File

@ -0,0 +1,29 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('osTicketReportEmail', {
description: 'Sends the buyer waste email',
accessType: 'WRITE',
accepts: [],
returns: {
type: ['object'],
root: true
},
http: {
path: '/osticket-report-email',
verb: 'POST'
}
});
Self.osTicketReportEmail = async ctx => {
const models = Self.app.models;
const printConfig = await models.PrintConfig.findOne();
const email = new Email('osticket-report', {
recipient: printConfig.itRecipient,
lang: ctx.req.getLocale()
});
return email.send();
};
};

View File

@ -19,11 +19,11 @@ describe('getStarredModules()', () => {
});
it(`should return the starred modules for a given user`, async() => {
const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'Clients', position: 1});
const newStarred = await app.models.StarredModule.create({workerFk: 9, moduleFk: 'customer', position: 1});
const starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(1);
expect(starredModules[0].moduleFk).toEqual('Clients');
expect(starredModules[0].moduleFk).toEqual('customer');
// restores
await app.models.StarredModule.destroyById(newStarred.id);

View File

@ -26,29 +26,29 @@ describe('setPosition()', () => {
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Orders'
moduleFk: 'order'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
let clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(1);
expect(clients.position).toEqual(2);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'left', options);
await app.models.StarredModule.setPosition(ctx, 'customer', 'left', options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
orders = await app.models.StarredModule.findOne(filter, options);
expect(clients.position).toEqual(1);
@ -67,29 +67,29 @@ describe('setPosition()', () => {
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Orders'
moduleFk: 'order'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
let clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(1);
expect(clients.position).toEqual(2);
await app.models.StarredModule.setPosition(ctx, 'Orders', 'right', options);
await app.models.StarredModule.setPosition(ctx, 'order', 'right', options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(2);
@ -108,35 +108,35 @@ describe('setPosition()', () => {
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Items'
moduleFk: 'item'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'item', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'claim', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'zone', options);
const items = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Claims';
filter.where.moduleFk = 'claim';
const claims = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
let clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Zones';
filter.where.moduleFk = 'zone';
const zones = await app.models.StarredModule.findOne(filter, options);
expect(items.position).toEqual(1);
@ -145,12 +145,12 @@ describe('setPosition()', () => {
expect(orders.position).toEqual(4);
expect(zones.position).toEqual(5);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'right', options);
await app.models.StarredModule.setPosition(ctx, 'customer', 'right', options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
clients = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(3);
@ -169,31 +169,31 @@ describe('setPosition()', () => {
const filter = {
where: {
workerFk: ctx.req.accessToken.userId,
moduleFk: 'Items'
moduleFk: 'item'
}
};
try {
const options = {transaction: tx};
await app.models.StarredModule.toggleStarredModule(ctx, 'Items', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Clients', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'Zones', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'item', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'customer', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'claim', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'order', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'zone', options);
const items = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
let clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Claims';
filter.where.moduleFk = 'claim';
const claims = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
let orders = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Zones';
filter.where.moduleFk = 'zone';
const zones = await app.models.StarredModule.findOne(filter, options);
expect(items.position).toEqual(1);
@ -202,13 +202,13 @@ describe('setPosition()', () => {
expect(orders.position).toEqual(4);
expect(zones.position).toEqual(5);
await app.models.StarredModule.toggleStarredModule(ctx, 'Claims', options);
await app.models.StarredModule.setPosition(ctx, 'Clients', 'right', options);
await app.models.StarredModule.toggleStarredModule(ctx, 'claim', options);
await app.models.StarredModule.setPosition(ctx, 'customer', 'right', options);
filter.where.moduleFk = 'Clients';
filter.where.moduleFk = 'customer';
clients = await app.models.StarredModule.findOne(filter, options);
filter.where.moduleFk = 'Orders';
filter.where.moduleFk = 'order';
orders = await app.models.StarredModule.findOne(filter, options);
expect(orders.position).toEqual(2);

View File

@ -21,15 +21,15 @@ describe('toggleStarredModule()', () => {
});
it('should create a new starred module and then remove it by calling the method again with same args', async() => {
const starredModule = await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
const starredModule = await app.models.StarredModule.toggleStarredModule(ctx, 'order');
let starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(1);
expect(starredModule.moduleFk).toEqual('Orders');
expect(starredModule.moduleFk).toEqual('order');
expect(starredModule.workerFk).toEqual(activeCtx.accessToken.userId);
expect(starredModule.position).toEqual(starredModules.length);
await app.models.StarredModule.toggleStarredModule(ctx, 'Orders');
await app.models.StarredModule.toggleStarredModule(ctx, 'order');
starredModules = await app.models.StarredModule.getStarredModules(ctx);
expect(starredModules.length).toEqual(0);

View File

@ -77,6 +77,21 @@
"Module": {
"dataSource": "vn"
},
"Notification": {
"dataSource": "vn"
},
"NotificationAcl": {
"dataSource": "vn"
},
"NotificationConfig": {
"dataSource": "vn"
},
"NotificationQueue": {
"dataSource": "vn"
},
"NotificationSubscription": {
"dataSource": "vn"
},
"Province": {
"dataSource": "vn"
},
@ -101,6 +116,9 @@
"Town": {
"dataSource": "vn"
},
"Url": {
"dataSource": "vn"
},
"UserConfig": {
"dataSource": "vn"
},
@ -116,8 +134,14 @@
"OsTicket": {
"dataSource": "osticket"
},
"OsTicketConfig": {
"dataSource": "vn"
},
"Edi": {
"dataSource": "vn"
},
"PrintConfig": {
"dataSource": "vn"
}
}

View File

@ -1,4 +1,7 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = Self => {
require('../methods/account/login')(Self);
@ -6,7 +9,9 @@ module.exports = Self => {
require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
// Validations
@ -26,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password);
});
Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
if (!ctx.args || !ctx.args.data.email) return;
const models = Self.app.models;
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 userId = ctx.instance.id;
const user = await models.user.findById(userId);
class Mailer {
async send(verifyOptions, cb) {
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to,
lang: ctx.req.getLocale()
};
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
}
}
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
};
await user.verify(options);
});
Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data',
accepts: [
{
arg: 'ctx',
type: 'Object',
type: 'object',
http: {source: 'context'}
}
],
returns: {
type: 'Object',
type: 'object',
root: true
},
http: {
@ -57,7 +107,7 @@ module.exports = Self => {
*
* @param {Integer} userId The user id
* @param {String} name The role name
* @param {Object} options Options
* @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise
*/
Self.hasRole = async function(userId, name, options) {
@ -69,8 +119,8 @@ module.exports = Self => {
* Get all user roles.
*
* @param {Integer} userId The user id
* @param {Object} options Options
* @return {Object} User role list
* @param {object} options Options
* @return {object} User role list
*/
Self.getRoles = async(userId, options) => {
let result = await Self.rawSql(
@ -81,7 +131,7 @@ module.exports = Self => {
WHERE u.id = ?`, [userId], options);
let roles = [];
for (role of result)
for (const role of result)
roles.push(role.name);
return roles;

View File

@ -40,6 +40,9 @@
"email": {
"type": "string"
},
"emailVerified": {
"type": "boolean"
},
"created": {
"type": "date"
},
@ -48,6 +51,9 @@
},
"image": {
"type": "string"
},
"hasGrant": {
"type": "boolean"
}
},
"relations": {
@ -85,6 +91,13 @@
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "logout",
@ -99,6 +112,13 @@
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"property": "privileges",
"accessType": "*",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
}
]
}

View File

@ -4,4 +4,23 @@ module.exports = Self => {
require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self);
require('../methods/chat/sendQueued')(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]);
ctx.instance.message = message;
});
};

View File

@ -32,6 +32,9 @@
},
"attempts": {
"type": "number"
},
"error": {
"type": "string"
}
},
"acls": [{

View File

@ -3,4 +3,5 @@ module.exports = Self => {
require('../methods/collection/newCollection')(Self);
require('../methods/collection/getSectors')(Self);
require('../methods/collection/setSaleQuantity')(Self);
require('../methods/collection/previousLabel')(Self);
};

View File

@ -20,7 +20,6 @@
"type": "date"
}
},
"scope": {
"where" :{
"expired": null

View File

@ -9,17 +9,29 @@
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
"type": "number"
},
"date": {
"created": {
"type": "date"
},
"m3":{
"longitude":{
"type": "number"
},
"warehouseFk":{
"latitude":{
"type": "number"
},
"dated":{
"type": "date"
},
"ticketFk":{
"type": "number"
}
},
"relations": {
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk"
}
}
}

View File

@ -16,7 +16,7 @@
"url": {
"type": "string"
},
"token": {
"cookie": {
"type": "string"
}
},

View File

@ -1,4 +1,7 @@
module.exports = Self => {
require('../methods/docuware/download')(Self);
require('../methods/docuware/upload')(Self);
require('../methods/docuware/checkFile')(Self);
require('../methods/docuware/deliveryNoteEmail')(Self);
require('../methods/docuware/core')(Self);
};

View File

@ -19,20 +19,14 @@
"fileCabinetName": {
"type": "string"
},
"action": {
"type": "string"
},
"dialogName": {
"type": "string"
},
"find": {
"findById": {
"type": "string"
}
},
"acls": [
{
"property": "*",
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -77,16 +77,15 @@ module.exports = Self => {
const newImage = await Self.upsertWithWhere(data, {
name: fileName,
collectionFk: collectionName,
updated: (new Date).getTime()
updated: Date.vnNow()
}, myOptions);
// Resizes and saves the image
const container = await models.ImageContainer.container(collectionName);
const rootPath = container.client.root;
const collectionDir = path.join(rootPath, collectionName);
const file = `${fileName}.png`;
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
const dstFile = path.join(dstDir, fileName);
const buffer = readChunk.sync(srcFilePath, 0, 12);
const type = imageType(buffer);
@ -102,7 +101,8 @@ module.exports = Self => {
width: bmpData.width,
height: bmpData.height,
channels: 4
}
},
failOn: 'none'
};
}
@ -120,7 +120,7 @@ module.exports = Self => {
const sizes = collection.sizes();
for (let size of sizes) {
const dstDir = path.join(collectionDir, `${size.width}x${size.height}`);
const dstFile = path.join(dstDir, file);
const dstFile = path.join(dstDir, fileName);
const resizeOpts = {
withoutEnlargement: true,
fit: size.crop ? 'cover' : 'inside'

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/notification/send')(Self);
require('../methods/notification/clean')(Self);
};

View File

@ -0,0 +1,30 @@
{
"name": "Notification",
"base": "VnModel",
"options": {
"mysql": {
"table": "util.notification"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
}
},
"relations": {
"subscription": {
"type": "hasMany",
"model": "NotificationSubscription",
"foreignKey": "notificationFk"
}
}
}

View File

@ -0,0 +1,31 @@
{
"name": "NotificationAcl",
"base": "VnModel",
"options": {
"mysql": {
"table": "util.notificationAcl"
}
},
"properties":{
"notificationFk": {
"id": true,
"type": "number"
},
"roleFk":{
"id": true,
"type": "number"
}
},
"relations": {
"notification": {
"type": "belongsTo",
"model": "Notification",
"foreignKey": "notificationFk"
},
"role": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "roleFk"
}
}
}

View File

@ -1,9 +1,9 @@
{
"name": "Sector",
"name": "NotificationConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "sector"
"table": "util.notificationConfig"
}
},
"properties": {
@ -12,9 +12,8 @@
"id": true,
"description": "Identifier"
},
"description": {
"type": "string",
"required": true
"cleanDays": {
"type": "number"
}
}
}

View File

@ -0,0 +1,38 @@
{
"name": "NotificationQueue",
"base": "VnModel",
"options": {
"mysql": {
"table": "util.notificationQueue"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"params": {
"type": "string"
},
"status": {
"type": "string"
},
"created": {
"type": "date"
}
},
"relations": {
"notification": {
"type": "belongsTo",
"model": "Notification",
"foreignKey": "notificationFk",
"primaryKey": "name"
},
"author": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "authorFk"
}
}
}

View File

@ -0,0 +1,62 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.observe('before save', async function(ctx) {
const models = Self.app.models;
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');
});
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'
}
});
Self.deleteNotification = async function(ctx, notificationId) {
const models = Self.app.models;
const user = ctx.req.accessToken.userId;
const modifiedUser = await getUserToModify(notificationId, null, models);
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
}
});
}
};

View File

@ -0,0 +1,36 @@
{
"name": "NotificationSubscription",
"base": "VnModel",
"options": {
"mysql": {
"table": "util.notificationSubscription"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Primary key"
},
"notificationFk": {
"type": "number",
"description": "Foreign key to Notification"
},
"userFk": {
"type": "number",
"description": "Foreign key to Account"
}
},
"relations": {
"notification": {
"type": "belongsTo",
"model": "Notification",
"foreignKey": "notificationFk"
},
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -0,0 +1,58 @@
{
"name": "OsTicketConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "osTicketConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"host": {
"type": "string"
},
"user": {
"type": "string"
},
"password": {
"type": "string"
},
"oldStatus": {
"type": "string"
},
"newStatusId": {
"type": "number"
},
"day": {
"type": "number"
},
"comment": {
"type": "string"
},
"hostDb": {
"type": "string"
},
"userDb": {
"type": "string"
},
"passwordDb": {
"type": "string"
},
"portDb": {
"type": "number"
},
"responseType": {
"type": "string"
},
"fromEmailId": {
"type": "number"
},
"replyTo": {
"type": "string"
}
}
}

4
back/models/osticket.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/osticket/osTicketReportEmail')(Self);
require('../methods/osticket/closeTicket')(Self);
};

View File

@ -1,12 +1,5 @@
{
"name": "OsTicket",
"base": "VnModel",
"acls": [{
"property": "validations",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
"base": "VnModel"
}

View File

@ -0,0 +1,29 @@
{
"name": "PrintConfig",
"description": "Print config",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.printConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"itRecipient": {
"type": "string"
},
"incidencesEmail": {
"type": "string"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -1,14 +1,14 @@
const app = require('vn-loopback/server/server');
const models = require('vn-loopback/server/server').models;
describe('loopback model Account', () => {
it('should return true if the user has the given role', async() => {
let result = await app.models.Account.hasRole(1, 'employee');
let result = await models.Account.hasRole(1, 'employee');
expect(result).toBeTruthy();
});
it('should return false if the user doesnt have the given role', async() => {
let result = await app.models.Account.hasRole(1, 'administrator');
let result = await models.Account.hasRole(1, 'administrator');
expect(result).toBeFalsy();
});

View File

@ -0,0 +1,74 @@
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() => {
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 2;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
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;
}
});
it('Should delete a notification if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 4;
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;
}
});
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;
}
});
});

View File

@ -0,0 +1,32 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('account recoverPassword()', () => {
const userId = 1107;
const activeCtx = {
accessToken: {userId: userId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should send email with token', async() => {
const userId = 1107;
const user = await models.Account.findById(userId);
await models.Account.recoverPassword(user.email);
const result = await models.AccessToken.findOne({where: {userId: userId}});
expect(result).toBeDefined();
});
});

25
back/models/url.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "Url",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.url"
}
},
"properties": {
"appName": {
"type": "string",
"required": true,
"id": 1
},
"environment": {
"type": "string",
"required": true,
"id": 2
},
"url": {
"type": "string",
"required": true
}
}
}

27
back/models/user.js Normal file
View File

@ -0,0 +1,27 @@
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
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 user = await Self.app.models.Account.findById(info.user.id);
const params = {
recipient: info.email,
lang: user.lang,
url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}`
};
const options = Object.assign({}, info.options);
for (const param in options)
params[param] = options[param];
const email = new Email(options.emailTemplate, params);
return email.send();
});
};

View File

@ -24,9 +24,6 @@
},
"isManaged":{
"type": "boolean"
},
"hasStowaway":{
"type": "boolean"
}
},
"acls": [

View File

@ -18,6 +18,7 @@
"modules/supplier/front/**/*",
"modules/ticket/front/**/*",
"modules/travel/front/**/*",
"modules/shelving/front/**/*",
"modules/worker/front/**/*",
"modules/zone/front/**/*"
]

View File

@ -7,13 +7,18 @@ process.on('warning', warning => {
console.log(warning.stack);
});
process.on('exit', async function() {
if (container) await container.rm();
});
let container;
async function test() {
let isCI = false;
if (process.argv[2] === 'ci')
isCI = true;
const container = new Docker();
container = new Docker();
await container.run(isCI);
dataSources = JSON.parse(JSON.stringify(dataSources));
@ -25,7 +30,10 @@ async function test() {
const bootOptions = {dataSources};
const app = require('vn-loopback/server/server');
app.boot(bootOptions);
await new Promise((resolve, reject) => {
app.boot(bootOptions,
err => err ? reject(err) : resolve());
});
const Jasmine = require('jasmine');
const jasmine = new Jasmine();
@ -46,6 +54,7 @@ async function test() {
jasmine.addReporter(new JunitReporter.JUnitXmlReporter());
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.exitOnCompletion = true;
}
const backSpecs = [
@ -60,11 +69,10 @@ async function test() {
helpers: [],
});
jasmine.exitOnCompletion = false;
await jasmine.execute();
if (app) await app.disconnect();
if (container) await container.rm();
console.log('app disconnected & container removed');
console.log('App disconnected & container removed');
}
test();

View File

@ -0,0 +1,49 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ClientConsumptionQueue', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'deliveryNotePdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'deliveryNoteEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'deliveryNoteCsvPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'deliveryNoteCsvEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientWelcomeHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientWelcomeEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'printerSetupHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'printerSetupEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'sepaCoreEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorStHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorStEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorNdHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorNdEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationHtml', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Client', 'consumptionSendQueued', 'WRITE', 'ALLOW', 'ROLE', 'system'),
('InvoiceOut', 'invoiceEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('InvoiceOut', 'exportationPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('InvoiceOut', 'sendQueued', 'WRITE', 'ALLOW', 'ROLE', 'system'),
('Ticket', 'invoiceCsvPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'invoiceCsvEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Supplier', 'campaignMetricsPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Supplier', 'campaignMetricsEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Travel', 'extraCommunityPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Travel', 'extraCommunityEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Entry', 'entryOrderPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('OsTicket', 'osTicketReportEmail', 'WRITE', 'ALLOW', 'ROLE', 'system'),
('Item', 'buyerWasteEmail', 'WRITE', 'ALLOW', 'ROLE', 'system'),
('Claim', 'claimPickupPdf', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Claim', 'claimPickupEmail', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Item', 'labelPdf', 'READ', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Sector','*','READ','ALLOW','ROLE','employee');
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Sector','*','WRITE','ALLOW','ROLE','employee');

View File

@ -1,3 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('SupplierAgencyTerm', '*', '*', 'ALLOW', 'ROLE', 'administrative');
('Receipt', 'receiptPdf', '*', 'ALLOW', 'ROLE', 'salesAssistant');

View File

@ -0,0 +1,9 @@
create table `vn`.`clientConsumptionQueue`
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
params json not null,
queued datetime default current_timestamp() not null,
printed datetime null,
status varchar(50) default '' null
)
comment 'Queue for client consumption PDF mailing';

View File

@ -0,0 +1 @@
rename table `vn`.`invoiceOut_queue` to `vn`.`invoiceOutQueue`;

View File

@ -0,0 +1,5 @@
ALTER TABLE `vn`.`itemConfig`
ADD id int null PRIMARY KEY first;
ALTER TABLE `vn`.`itemConfig`
ADD wasteRecipients VARCHAR(50) NOT NULL comment 'Weekly waste report schedule recipients';

View File

@ -0,0 +1,10 @@
create table `salix`.`printConfig`
(
id int auto_increment,
itRecipient varchar(50) null comment 'IT recipients for report mailing',
incidencesEmail varchar(50) null comment 'CAU destinatary email',
constraint printConfig_pk
primary key (id)
)
comment 'Print service config';

View File

@ -0,0 +1,100 @@
DROP TRIGGER IF EXISTS vn.sale_afterUpdate;
USE vn;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`sale_afterUpdate`
AFTER UPDATE ON `sale`
FOR EACH ROW
BEGIN
DECLARE vIsToSendMail BOOL;
DECLARE vPickedLines INT;
DECLARE vCollectionFk INT;
DECLARE vUserRole VARCHAR(255);
IF !(NEW.id <=> OLD.id)
OR !(NEW.ticketFk <=> OLD.ticketFk)
OR !(NEW.itemFk <=> OLD.itemFk)
OR !(NEW.quantity <=> OLD.quantity)
OR !(NEW.created <=> OLD.created)
OR !(NEW.isPicked <=> OLD.isPicked) THEN
CALL stock.log_add('sale', NEW.id, OLD.id);
END IF;
IF !(NEW.price <=> OLD.price)
OR !(NEW.ticketFk <=> OLD.ticketFk)
OR !(NEW.itemFk <=> OLD.itemFk)
OR !(NEW.quantity <=> OLD.quantity)
OR !(NEW.discount <=> OLD.discount) THEN
CALL ticket_requestRecalc(NEW.ticketFk);
CALL ticket_requestRecalc(OLD.ticketFk);
END IF;
IF !(OLD.ticketFk <=> NEW.ticketFk) THEN
UPDATE ticketRequest SET ticketFk = NEW.ticketFk
WHERE saleFk = NEW.id;
END IF;
SELECT account.myUser_getName() INTO vUserRole;
SELECT account.user_getMysqlRole(vUserRole) INTO vUserRole;
IF !(OLD.quantity <=> NEW.quantity) THEN
SELECT COUNT(*) INTO vIsToSendMail
FROM vncontrol.inter i
JOIN vn.state s ON s.id = i.state_id
WHERE s.code='PACKED'
AND i.Id_Ticket = OLD.ticketFk
AND vUserRole IN ('salesPerson', 'salesTeamBoss')
LIMIT 1;
IF vIsToSendMail THEN
CALL vn.mail_insert('jefesventas@verdnatura.es',
'noreply@verdnatura.es',
CONCAT('Ticket ', OLD.ticketFk ,' modificada cantidad tras encajado'),
CONCAT('Ticket <a href="https://salix.verdnatura.es/#!/ticket/', OLD.ticketFk ,'/log">', OLD.ticketFk ,'</a>. <br>',
'Modificada la catidad de ', OLD.quantity, ' a ' , NEW.quantity,
' del artículo ', OLD.itemFk, ' tras estado encajado del ticket. <br>',
'Este email se ha generado automáticamente' )
);
END IF;
IF (OLD.quantity > NEW.quantity) THEN
INSERT INTO saleComponent(saleFk, componentFk, value)
SELECT NEW.id, cm.id, sc.value
FROM saleComponent sc
JOIN component cd ON cd.id = sc.componentFk
JOIN component cm ON cm.code = 'mana'
WHERE saleFk = NEW.id AND cd.code = 'lastUnitsDiscount'
ON DUPLICATE KEY UPDATE value = sc.value + VALUES(value);
DELETE sc.*
FROM vn.saleComponent sc
JOIN component c ON c.id = sc.componentFk
WHERE saleFk = NEW.id AND c.code = 'lastUnitsDiscount';
END IF;
INSERT IGNORE INTO `vn`.`routeRecalc` (`routeFk`)
SELECT r.id
FROM vn.sale s
JOIN vn.ticket t ON t.id = s.ticketFk
JOIN vn.route r ON r.id = t.routeFk
WHERE r.isOk = FALSE
AND s.id = NEW.id
AND r.created >= CURDATE()
GROUP BY r.id;
END IF;
IF !(ABS(NEW.isPicked) <=> ABS(OLD.isPicked)) AND NEW.quantity > 0 THEN
UPDATE vn.collection c
JOIN vn.ticketCollection tc ON tc.collectionFk = c.id AND tc.ticketFk = NEW.ticketFk
SET c.salePickedCount = c.salePickedCount + IF(NEW.isPicked != 0, 1, -1);
END IF;
IF !(NEW.quantity <=> OLD.quantity) AND (NEW.quantity = 0 OR OLD.quantity = 0) THEN
UPDATE vn.collection c
JOIN vn.ticketCollection tc ON tc.collectionFk = c.id AND tc.ticketFk = NEW.ticketFk
SET c.saleTotalCount = c.saleTotalCount + IF(OLD.quantity = 0, 1, -1);
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,6 @@
alter table `vn`.`sample`
add model VARCHAR(25) null comment 'Model name in plural';
UPDATE vn.sample t
SET t.model = 'Clients'
WHERE t.id IN(12, 13, 14, 15, 16, 18, 19, 20);

View File

@ -0,0 +1 @@
ALTER TABLE `account`.`user` ADD hasGrant TINYINT(1) NOT NULL;

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalId)
VALUES ('WorkerDisableExcluded','*','*','ALLOW','hr');

View File

@ -1,2 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES('EducationLevel', '*', '*', 'ALLOW', 'ROLE', 'employee');
VALUES
('Business', '*', '*', 'ALLOW', 'ROLE', 'hr');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Sale', 'usesMana', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,2 @@
INSERT INTO `vn`.`payDem` (id,payDem)
VALUES (7,'0');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative');

View File

@ -1,2 +1,2 @@
INSERT INTO `salix`.`ACL` (model,property,accessType,principalId)
VALUES ('AgencyTerm','*','*','administrative');
VALUES ('Supplier','newSupplier','WRITE','administrative');

View File

@ -0,0 +1,23 @@
DROP FUNCTION IF EXISTS `util`.`notification_send`;
DELIMITER $$
CREATE DEFINER=`root`@`localhost` FUNCTION `util`.`notification_send`(vNotificationName VARCHAR(255), vParams TEXT, vAuthorFk INT)
RETURNS INT
MODIFIES SQL DATA
BEGIN
/**
* Sends a notification.
*
* @param vNotificationName The notification name
* @param vParams The notification parameters formatted as JSON
* @param vAuthorFk The notification author or %NULL if there is no author
* @return The notification id
*/
INSERT INTO notificationQueue
SET notificationFk = vNotificationName,
params = vParams,
authorFk = vAuthorFk;
RETURN LAST_INSERT_ID();
END$$
DELIMITER ;

View File

@ -0,0 +1,63 @@
USE util;
CREATE TABLE notification(
id INT PRIMARY KEY,
`name` VARCHAR(255) UNIQUE,
`description` VARCHAR(255)
);
CREATE TABLE notificationAcl(
notificationFk INT,
roleFk INT(10) unsigned,
PRIMARY KEY(notificationFk, roleFk)
);
ALTER TABLE `util`.`notificationAcl` ADD CONSTRAINT `notificationAcl_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE `util`.`notificationAcl` ADD CONSTRAINT `notificationAcl_ibfk_2` FOREIGN KEY (`roleFk`) REFERENCES `account`.`role`(`id`)
ON DELETE RESTRICT
ON UPDATE CASCADE;
CREATE TABLE notificationSubscription(
notificationFk INT,
userFk INT(10) unsigned,
PRIMARY KEY(notificationFk, userFk)
);
ALTER TABLE `util`.`notificationSubscription` ADD CONSTRAINT `notificationSubscription_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE `util`.`notificationSubscription` ADD CONSTRAINT `notificationSubscription_ibfk_2` FOREIGN KEY (`userFk`) REFERENCES `account`.`user`(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
CREATE TABLE notificationQueue(
id INT PRIMARY KEY AUTO_INCREMENT,
notificationFk VARCHAR(255),
params JSON,
authorFk INT(10) unsigned NULL,
`status` ENUM('pending', 'sent', 'error') NOT NULL DEFAULT 'pending',
created DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX(notificationFk),
INDEX(authorFk),
INDEX(status)
);
ALTER TABLE `util`.`notificationQueue` ADD CONSTRAINT `nnotificationQueue_ibfk_1` FOREIGN KEY (`notificationFk`) REFERENCES `util`.`notification` (`name`)
ON DELETE CASCADE
ON UPDATE CASCADE;
ALTER TABLE `util`.`notificationQueue` ADD CONSTRAINT `notificationQueue_ibfk_2` FOREIGN KEY (`authorFk`) REFERENCES `account`.`user`(`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
CREATE TABLE notificationConfig(
id INT PRIMARY KEY AUTO_INCREMENT,
cleanDays MEDIUMINT
);
INSERT INTO notificationConfig
SET cleanDays = 90;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`supplier` MODIFY COLUMN payMethodFk tinyint(3) unsigned NULL;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`supplier` MODIFY COLUMN supplierActivityFk varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL NULL;

View File

@ -0,0 +1,35 @@
drop procedure `vn`.`ticket_closeByTicket`;
DELIMITER $$
$$
create
definer = `root`@`localhost` procedure `vn`.`ticket_closeByTicket`(IN vTicketFk int)
BEGIN
/**
* Inserta el ticket en la tabla temporal
* para ser cerrado.
*
* @param vTicketFk Id del ticket
*/
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close;
CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY (
SELECT
t.id AS ticketFk
FROM ticket t
JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered')
AND t.id = vTicketFk
AND t.refFk IS NULL
GROUP BY t.id);
CALL ticket_close();
DROP TEMPORARY TABLE tmp.ticket_close;
END$$
DELIMITER ;

View File

@ -0,0 +1,9 @@
CREATE TABLE `vn`.`zipConfig` (
`id` double(10,2) NOT NULL,
`maxSize` int(11) DEFAULT NULL COMMENT 'in MegaBytes',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('ZipConfig', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,5 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ClaimRma', '*', 'READ', 'ALLOW', 'ROLE', 'claimManager'),
('ClaimRma', '*', 'WRITE', 'ALLOW', 'ROLE', 'claimManager');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`claim` ADD rma varchar(100) NULL ;

View File

@ -0,0 +1,7 @@
CREATE TABLE `vn`.`claimRma` (
id INT UNSIGNED auto_increment NOT NULL PRIMARY KEY,
code varchar(100) NOT NULL,
created timestamp DEFAULT current_timestamp() NOT NULL,
workerFk INTEGER UNSIGNED NOT NULL
)
ENGINE=InnoDB;

Some files were not shown because too many files have changed in this diff Show More