Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5712-workerDms_integrateDocuware
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Alex Moreno 2023-06-28 14:30:20 +02:00
commit befa385830
327 changed files with 4002 additions and 3085 deletions

View File

@ -1,6 +1,6 @@
extends: [eslint:recommended, google, plugin:jasmine/recommended]
parserOptions:
ecmaVersion: 2018
ecmaVersion: 2020
sourceType: "module"
plugins:
- jasmine
@ -17,7 +17,7 @@ rules:
camelcase: 0
default-case: 0
no-eq-null: 0
no-console: ["error"]
no-console: ["warn"]
no-warning-comments: 0
no-empty: [error, allowEmptyCatch: true]
complexity: 0

View File

@ -5,14 +5,42 @@ 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).
## [2324.01] - 2023-06-08
## [2328.01] - 2023-07-13
### Added
-
### Changed
-
### Fixed
## [2326.01] - 2023-06-29
### Added
- (Entradas -> Correo) Al cambiar el tipo de cambio enviará un correo a las personas designadas
- (General -> Históricos) Botón para ver el estado del registro en cada punto
- (General -> Históricos) Al filtar por registro se muestra todo el histórial desde que fue creado
### Changed
- (General -> Históricos) Los registros se muestran agrupados por usuario y entidad
- (Facturas -> Facturación global) Optimizada, generación de PDFs y notificaciones en paralelo
### Fixed
- (General -> Históricos) Duplicidades eliminadas
- (Facturas -> Facturación global) Solucionados fallos que paran el proceso
## [2324.01] - 2023-06-15
### Added
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
- (General -> Desplegables) Mejorada eficiencia de carga de datos
- (General -> Históricos) Ahora, ademas de los ids, se muestra la descripión de los atributos
- (General -> Históricos) Botón para hacer más ágil mostrar sólo los cambios en un registro
- (General -> Históricos) Filtro por cambios
### Changed
- (General -> Permisos) Mejorada seguridad
- (General -> Históricos) Elementos de la interfaz reorganizados para hacerla más ágil e intuitiva
### Fixed
-
@ -28,7 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador'
- (Tickets -> Expediciones) Interfaz mejorada y contador añadido
### Fixed
- (Tickets -> Líneas) Se permite hacer split de líneas al mismo ticket
- (Tickets -> Cambiar estado) Ahora muestra la lista completa de todos los estados

View File

@ -11,9 +11,9 @@ RUN apt-get update \
ca-certificates \
gnupg2 \
graphicsmagick \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2
&& npm install -g npm@9.6.6
# Puppeteer

8
Jenkinsfile vendored
View File

@ -39,7 +39,7 @@ pipeline {
NODE_ENV = ""
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci'
}
@ -57,14 +57,14 @@ pipeline {
parallel {
stage('Frontend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
}
}
}
stage('Backend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm run test:back:ci'
}
}
@ -80,7 +80,7 @@ pipeline {
CREDENTIALS = credentials('docker-registry')
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'gulp build'
}

View File

@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
* Node.js = 14.x LTS
* Node.js >= 16.x LTS
* Docker
* Git

View File

@ -30,11 +30,11 @@ module.exports = Self => {
Self.newCollection = async(ctx, collectionFk, sectorFk, vWagons) => {
let query = '';
const userId = ctx.req.accessToken.userId;
if (!collectionFk) {
const userId = ctx.req.accessToken.userId;
query = `CALL vn.collectionTrain_newBeta(?,?,?)`;
const [result] = await Self.rawSql(query, [sectorFk, vWagons, userId]);
const [result] = await Self.rawSql(query, [sectorFk, vWagons, userId], {userId});
if (result.length == 0)
throw new Error(`No collections for today`);
@ -42,16 +42,16 @@ module.exports = Self => {
}
query = `CALL vn.collectionTicket_get(?)`;
const [tickets] = await Self.rawSql(query, [collectionFk]);
const [tickets] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionSale_get(?)`;
const [sales] = await Self.rawSql(query, [collectionFk]);
const [sales] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionPlacement_get(?)`;
const [placements] = await Self.rawSql(query, [collectionFk]);
const [placements] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionSticker_print(?,?)`;
await Self.rawSql(query, [collectionFk, sectorFk]);
await Self.rawSql(query, [collectionFk, sectorFk], {userId});
return makeCollection(tickets, sales, placements, collectionFk);
};

View File

@ -2,8 +2,9 @@ const models = require('vn-loopback/server/server').models;
describe('docuware upload()', () => {
const userId = 9;
const ticketId = 10;
const ticketIds = [10];
const ctx = {
args: {ticketIds},
req: {
getLocale: () => {
return 'en';
@ -27,7 +28,7 @@ describe('docuware upload()', () => {
let error;
try {
await models.Docuware.upload(ctx, ticketId, fileCabinetName);
await models.Docuware.upload(ctx, ticketIds, fileCabinetName);
} catch (e) {
error = e.message;
}

View File

@ -3,29 +3,34 @@ const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('upload', {
description: 'Upload an docuware PDF',
description: 'Upload docuware PDFs',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
description: 'The ticket id',
http: {source: 'path'}
arg: 'ticketIds',
type: ['number'],
description: 'The ticket ids',
required: true
},
{
arg: 'fileCabinet',
type: 'string',
description: 'The file cabinet'
description: 'The file cabinet',
required: true
}
],
returns: [],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/upload`,
path: `/upload`,
verb: 'POST'
}
});
Self.upload = async function(ctx, id, fileCabinet) {
Self.upload = async function(ctx, ticketIds, fileCabinet) {
delete ctx.args.ticketIds;
const models = Self.app.models;
const action = 'store';
@ -33,18 +38,20 @@ module.exports = Self => {
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
const uploaded = [];
for (id of ticketIds) {
// get delivery note
ctx.args.id = id;
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']
fields: ['id', 'name', 'fi']
}
}]
});
@ -70,7 +77,7 @@ module.exports = Self => {
{
'FieldName': 'NOMBRE_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().socialName,
'Item': ticket.client().name + ' - ' + id,
},
{
'FieldName': 'FECHA_FACTURA',
@ -128,9 +135,17 @@ module.exports = Self => {
},
};
return await axios.post(uploadUri, data, uploadOptions)
.catch(() => {
throw new UserError('Failed to upload file');
});
try {
await axios.post(uploadUri, data, uploadOptions);
} catch (err) {
const $t = ctx.req.__;
const message = $t('Failed to upload delivery note', {id});
if (uploaded.length)
await models.TicketTracking.setDelivered(ctx, uploaded);
throw new UserError(message);
}
uploaded.push(id);
}
return models.TicketTracking.setDelivered(ctx, ticketIds);
};
};

View File

@ -16,14 +16,14 @@ module.exports = Self => {
}
});
Self.updateData = async() => {
Self.updateData = async ctx => {
const models = Self.app.models;
// Get files checksum
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const options = {transaction: tx, userId: ctx.req.accessToken.userId};
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
const updatableFiles = [];

View File

@ -24,6 +24,7 @@ module.exports = Self => {
fields: ['email'],
where: {name: user}
});
if (!account) return;
user = account.email;
}

View File

@ -0,0 +1,39 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('renewToken', {
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
accessType: 'WRITE',
accepts: [],
returns: {
type: 'Object',
root: true
},
http: {
path: `/renewToken`,
verb: 'POST'
}
});
Self.renewToken = async function(ctx) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const created = ctx.req.accessToken.created;
const tokenId = ctx.req.accessToken.id;
const now = new Date();
const differenceMilliseconds = now - created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields: ['renewPeriod']});
if (differenceSeconds <= accessTokenConfig.renewPeriod)
throw new UserError(`The renew period has not been exceeded`);
await Self.logout(tokenId);
const user = await Self.findById(userId);
const accessToken = await user.createAccessToken();
return {token: accessToken.id, created: accessToken.created};
};
};

View File

@ -27,33 +27,46 @@ module.exports = Self => {
});
Self.signIn = async function(user, password) {
let models = Self.app.models;
const models = Self.app.models;
const usesEmail = user.indexOf('@') !== -1;
let token;
let usesEmail = user.indexOf('@') !== -1;
let userInfo = usesEmail
const userInfo = usesEmail
? {email: user}
: {username: user};
let instance = await Self.findOne({
const instance = await Self.findOne({
fields: ['username', 'password'],
where: userInfo
});
let where = usesEmail
const where = usesEmail
? {email: user}
: {name: user};
let vnUser = await Self.findOne({
fields: ['active'],
const vnUser = await Self.findOne({
fields: ['id', 'active', 'passExpired'],
where
});
let validCredentials = instance
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const validCredentials = instance
&& await instance.hasPassword(password);
if (validCredentials) {
if (!vnUser.active)
throw new UserError('User disabled');
if (vnUser.passExpired && vnUser.passExpired.getTime() <= today.getTime()) {
const changePasswordToken = await models.AccessToken.create({
scopes: ['change-password'],
userId: vnUser.id
});
const err = new UserError('Pass expired', 'passExpired');
err.details = {token: changePasswordToken};
throw err;
}
try {
await models.Account.sync(instance.username, password);
} catch (err) {
@ -63,6 +76,6 @@ module.exports = Self => {
let loginInfo = Object.assign({password}, userInfo);
token = await Self.login(loginInfo, 'user');
return {token: token.id};
return {token: token.id, created: token.created};
};
};

View File

@ -9,7 +9,7 @@ describe('VnUser signIn()', () => {
expect(login.token).toBeDefined();
await models.VnUser.signOut(ctx);
await models.VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token if the user doesnt exist but the client does', async() => {
@ -19,7 +19,7 @@ describe('VnUser signIn()', () => {
expect(login.token).toBeDefined();
await models.VnUser.signOut(ctx);
await models.VnUser.logout(ctx.req.accessToken.id);
});
});

View File

@ -1,42 +0,0 @@
const {models} = require('vn-loopback/server/server');
describe('VnUser signOut()', () => {
it('should logout and remove token after valid login', async() => {
let loginResponse = await models.VnUser.signOut('buyer', 'nightmare');
let accessToken = await models.AccessToken.findById(loginResponse.token);
let ctx = {req: {accessToken: accessToken}};
let logoutResponse = await models.VnUser.signOut(ctx);
let tokenAfterLogout = await models.AccessToken.findById(loginResponse.token);
expect(logoutResponse).toBeTrue();
expect(tokenAfterLogout).toBeNull();
});
it('should throw a 401 error when token is invalid', async() => {
let error;
let ctx = {req: {accessToken: {id: 'invalidToken'}}};
try {
response = await models.VnUser.signOut(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(401);
});
it('should throw an error when no token is passed', async() => {
let error;
let ctx = {req: {accessToken: null}};
try {
response = await models.VnUser.signOut(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
});
});

View File

@ -2,6 +2,14 @@
"AccountingType": {
"dataSource": "vn"
},
"AccessTokenConfig": {
"dataSource": "vn",
"options": {
"mysql": {
"table": "salix.accessTokenConfig"
}
}
},
"Bank": {
"dataSource": "vn"
},

View File

@ -0,0 +1,30 @@
{
"name": "AccessTokenConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "accessTokenConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"renewPeriod": {
"type": "number",
"required": true
},
"renewInterval": {
"type": "number",
"required": true
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -10,6 +10,9 @@ module.exports = function(Self) {
require('../methods/vn-user/recover-password')(Self);
require('../methods/vn-user/validate-token')(Self);
require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/renew-token')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
// Validations
@ -107,4 +110,81 @@ module.exports = function(Self) {
return email.send();
});
const _setPassword = Self.prototype.setPassword;
Self.prototype.setPassword = async function(newPassword, options, cb) {
if (cb === undefined && typeof options === 'function') {
cb = options;
options = undefined;
}
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
options = myOptions;
try {
await Self.rawSql(`CALL account.user_checkPassword(?)`, [newPassword], options);
await _setPassword.call(this, newPassword, options);
await this.updateAttribute('passExpired', null, options);
await Self.app.models.Account.sync(this.name, newPassword, null, options);
tx && await tx.commit();
cb && cb();
} catch (err) {
tx && await tx.rollback();
if (cb) cb(err); else throw err;
}
};
Self.sharedClass._methods.find(method => method.name == 'changePassword')
.accessScopes = ['change-password'];
// FIXME: https://redmine.verdnatura.es/issues/5761
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
// if (!ctx.args || !ctx.args.data.email) return;
// const loopBackContext = LoopBackContext.getCurrentContext();
// const httpCtx = {req: loopBackContext.active};
// const httpRequest = httpCtx.req.http.req;
// const headers = httpRequest.headers;
// const origin = headers.origin;
// const url = origin.split(':');
// 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 instance.verify(options);
// });
};

View File

@ -25,10 +25,7 @@
},
"password": {
"type": "string",
"required": true,
"mysql": {
"columnName": "bcryptPassword"
}
"required": true
},
"roleFk": {
"type": "number",
@ -42,9 +39,6 @@
"lang": {
"type": "string"
},
"bcryptPassword": {
"type": "string"
},
"active": {
"type": "boolean"
},
@ -62,6 +56,9 @@
},
"hasGrant": {
"type": "boolean"
},
"passExpired": {
"type": "date"
}
},
"relations": {
@ -121,5 +118,24 @@
"principalId": "$authenticated",
"permission": "ALLOW"
}
],
"scopes": {
"preview": {
"fields": [
"id",
"name",
"username",
"roleFk",
"nickname",
"lang",
"active",
"created",
"updated",
"image",
"hasGrant",
"realm",
"email"
]
}
}
}

View File

@ -4,4 +4,4 @@ apps:
instances: 1
max_restarts: 3
restart_delay: 15000
node_args: --tls-min-v1.0
node_args: --tls-min-v1.0 --openssl-legacy-provider

View File

@ -1,26 +1,18 @@
FROM mariadb:10.7.5
FROM mariadb:10.7.7
ENV MYSQL_ROOT_PASSWORD root
ENV TZ Europe/Madrid
ARG MOCKDATE=2001-01-01 11:00:00
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& curl -sL https://apt.verdnatura.es/conf/verdnatura.gpg | apt-key add - \
&& echo "deb http://apt.verdnatura.es/ jessie main" > /etc/apt/sources.list.d/vn.list \
&& apt-get update \
&& apt-get install -y vn-mariadb \
&& apt-get purge -y --auto-remove curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY docker/docker.cnf /etc/mysql/conf.d/
COPY \
docker/docker-start.sh \
docker/docker-init.sh \
docker/docker-temp-start.sh \
docker/docker-temp-stop.sh \
docker/docker-dump.sh \
docker/docker-start.sh \
docker/docker-structure.sh \
docker/docker-fixtures.sh \
/usr/local/bin/
RUN mkdir /mysql-data \
@ -31,26 +23,16 @@ WORKDIR /docker-boot
COPY \
import-changes.sh \
config.ini \
dump/mysqlPlugins.sql \
dump/structure.sql \
dump/mockDate.sql \
dump/dumpedFixtures.sql \
./
RUN gosu mysql docker-init.sh \
&& docker-dump.sh mysqlPlugins \
&& docker-dump.sh structure \
&& sed -i -e 's/@mockDate/'"$MOCKDATE"'/g' mockDate.sql \
&& docker-dump.sh mockDate \
&& docker-dump.sh dumpedFixtures \
&& gosu mysql docker-temp-stop.sh
RUN sed -i -e 's/@mockDate/'"$MOCKDATE"'/g' mockDate.sql \
&& gosu mysql docker-structure.sh
COPY changes ./changes
COPY dump/fixtures.sql ./
ARG STAMP=unknown
RUN gosu mysql docker-temp-start.sh \
&& ./import-changes.sh \
&& docker-dump.sh fixtures \
&& gosu mysql docker-temp-stop.sh
RUN gosu mysql docker-fixtures.sh
RUN echo "[INFO] -> Import finished" \
&& rm -rf /docker-boot

View File

@ -1,6 +1,5 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', '*', '*', 'ALLOW', 'ROLE', 'employee'),
('VnUser','acl','READ','ALLOW','ROLE','account'),
('VnUser','getCurrentUserData','READ','ALLOW','ROLE','account'),
('VnUser','changePassword', 'WRITE', 'ALLOW', 'ROLE', 'account'),

View File

@ -0,0 +1,127 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar.
*
* @param vDateFuture Fecha de los tickets que se quieren adelantar.
* @param vDateToAdvance Fecha a cuando se quiere adelantar.
* @param vWarehouseFk Almacén
*/
DECLARE vDateInventory DATE;
SELECT inventoried INTO vDateInventory FROM config;
DROP TEMPORARY TABLE IF EXISTS tmp.stock;
CREATE TEMPORARY TABLE tmp.stock
(itemFk INT PRIMARY KEY,
amount INT)
ENGINE = MEMORY;
INSERT INTO tmp.stock(itemFk, amount)
SELECT itemFk, SUM(quantity) amount FROM
(
SELECT itemFk, quantity
FROM itemTicketOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryIn
WHERE landed >= vDateInventory
AND landed < vDateFuture
AND isVirtualStock = FALSE
AND warehouseInFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseOutFk = vWarehouseFk
) t
GROUP BY itemFk HAVING amount != 0;
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT
origin.ticketFk futureId,
dest.ticketFk id,
dest.state,
origin.futureState,
origin.futureIpt,
dest.ipt,
origin.workerFk,
origin.futureLiters,
origin.futureLines,
dest.shipped,
origin.shipped futureShipped,
dest.totalWithVat,
origin.totalWithVat futureTotalWithVat,
dest.agency,
origin.futureAgency,
dest.lines,
dest.liters,
origin.futureLines - origin.hasStock AS notMovableLines,
(origin.futureLines = origin.hasStock) AS isFullMovable,
origin.classColor futureClassColor,
dest.classColor
FROM (
SELECT
s.ticketFk,
t.workerFk,
t.shipped,
t.totalWithVat,
st.name futureState,
t.addressFk,
am.name futureAgency,
count(s.id) futureLines,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt,
CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters,
SUM((s.quantity <= IFNULL(st.amount,0))) hasStock,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
LEFT JOIN tmp.stock st ON st.itemFk = i.id
WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) origin
JOIN (
SELECT
t.id ticketFk,
t.addressFk,
st.name state,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt,
t.shipped,
t.totalWithVat,
am.name agency,
CAST(SUM(litros) AS DECIMAL(10,0)) liters,
CAST(COUNT(*) AS DECIMAL(10,0)) `lines`,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance)
AND t.warehouseFk = vWarehouseFk
AND st.order <= 5
GROUP BY t.id
) dest ON dest.addressFk = origin.addressFk
WHERE origin.hasStock != 0;
DROP TEMPORARY TABLE tmp.stock;
END$$
DELIMITER ;

View File

@ -0,0 +1,74 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro
*
* @param vOriginDated Fecha en cuestión
* @param vFutureDated Fecha en el futuro a sondear
* @param vWarehouseFk Identificador de vn.warehouse
*/
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT sv.ticketFk id,
sub2.id futureId,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
st.name state,
sub2.iptd futureIpt,
sub2.state futureState,
t.clientFk,
t.warehouseFk,
ts.alertLevel,
t.shipped,
sub2.shipped futureShipped,
t.workerFk,
st.code stateCode,
sub2.code futureStateCode,
st.classColor,
sub2.classColor futureClassColor
FROM vn.saleVolume sv
JOIN vn.sale s ON s.id = sv.saleFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticket t ON t.id = sv.ticketFk
JOIN vn.address a ON a.id = t.addressFk
JOIN vn.province p ON p.id = a.provinceFk
JOIN vn.country c ON c.id = p.countryFk
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.alertLevel al ON al.id = ts.alertLevel
LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id
LEFT JOIN (
SELECT *
FROM (
SELECT
t.addressFk,
t.id,
t.shipped,
st.name state,
st.code,
st.classColor,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
WHERE t.shipped BETWEEN vFutureDated
AND util.dayend(vFutureDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) sub
GROUP BY sub.addressFk
) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id
WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated)
AND t.warehouseFk = vWarehouseFk
AND al.code = 'FREE'
AND tp.ticketFk IS NULL
GROUP BY sv.ticketFk
HAVING futureId;
END$$
DELIMITER ;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`ticket` MODIFY COLUMN warehouseFk smallint(6) unsigned DEFAULT NULL NULL;

View File

@ -0,0 +1,76 @@
ALTER TABLE `account`.`user` ADD passExpired DATE DEFAULT NULL;
DROP PROCEDURE `account`.`myUser_changePassword`;
DROP PROCEDURE `account`.`myUser_restorePassword`;
DROP PROCEDURE `account`.`user_changePassword`;
DROP PROCEDURE `account`.`user_restorePassword`;
DROP PROCEDURE `account`.`user_setPassword`;
ALTER TABLE account.`user` CHANGE password password__ char(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL COMMENT 'Deprecated';
ALTER TABLE account.`user` CHANGE bcryptPassword password varchar(512) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `account`.`user_beforeUpdate`
BEFORE UPDATE ON `user`
FOR EACH ROW
BEGIN
SET NEW.editorFk = account.myUser_getId();
IF !(NEW.`name` <=> OLD.`name`) THEN
CALL user_checkName (NEW.`name`);
END IF;
IF !(NEW.`password` <=> OLD.`password`) THEN
SET NEW.lastPassChange = util.VN_NOW();
END IF;
END$$
DELIMITER ;
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `account`.`accountDovecot` AS
select
`u`.`name` AS `name`,
`u`.`password` AS `password`
from
(`account`.`user` `u`
join `account`.`account` `a` on
(`a`.`id` = `u`.`id`))
where
`u`.`active` <> 0;
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `salix`.`User` AS
select
`account`.`user`.`id` AS `id`,
`account`.`user`.`realm` AS `realm`,
`account`.`user`.`name` AS `username`,
`account`.`user`.`password` AS `password`,
`account`.`user`.`email` AS `email`,
`account`.`user`.`emailVerified` AS `emailVerified`,
`account`.`user`.`verificationToken` AS `verificationToken`
from
`account`.`user`;
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn`.`workerTimeControlUserInfo` AS
select
`u`.`id` AS `userFk`,
`w`.`firstName` AS `name`,
`w`.`lastName` AS `surname`,
`u`.`name` AS `user`,
`u`.`password` AS `password`,
`wd`.`departmentFk` AS `departmentFk`,
left(`c`.`fi`,
8) AS `dni`
from
(((`account`.`user` `u`
join `vn`.`worker` `w` on
(`w`.`userFk` = `u`.`id`))
join `vn`.`client` `c` on
(`c`.`id` = `u`.`id`))
left join `vn`.`workerDepartment` `wd` on
(`wd`.`workerFk` = `w`.`id`));

View File

@ -0,0 +1,22 @@
CREATE TABLE `vn`.`travelConfig` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`warehouseInFk` smallint(6) unsigned NOT NULL DEFAULT 8 COMMENT 'Warehouse de origen',
`warehouseOutFk` smallint(6) unsigned NOT NULL DEFAULT 60 COMMENT 'Warehouse destino',
`agencyFk` int(11) NOT NULL DEFAULT 1378 COMMENT 'Agencia por defecto',
`companyFk` int(10) unsigned NOT NULL DEFAULT 442 COMMENT 'Compañía por defecto',
PRIMARY KEY (`id`),
KEY `travelConfig_FK` (`warehouseInFk`),
KEY `travelConfig_FK_1` (`warehouseOutFk`),
KEY `travelConfig_FK_2` (`agencyFk`),
KEY `travelConfig_FK_3` (`companyFk`),
CONSTRAINT `travelConfig_FK` FOREIGN KEY (`warehouseInFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_1` FOREIGN KEY (`warehouseOutFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_2` FOREIGN KEY (`agencyFk`) REFERENCES `agencyMode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_3` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Entry', 'addFromPackaging', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Entry', 'addFromBuy', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Supplier', 'getItemsPackaging', 'READ', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,8 @@
DELETE
FROM `salix`.`ACL`
WHERE model='Account' AND property='*' AND accessType='*';
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Account', '*', 'WRITE', 'ALLOW', 'ROLE', 'sysadmin'),
('Account', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'MailAliasAccount';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('MailAliasAccount', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('MailAliasAccount', '*', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'MailForward';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('MailForward', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('MailForward', '*', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,5 @@
DELETE FROM `salix`.`ACL` WHERE model = 'Role';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Role', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Role', '*', 'WRITE', 'ALLOW', 'ROLE', 'it');

View File

@ -0,0 +1,10 @@
DELETE
FROM `salix`.`ACL`
WHERE model = 'VnUser' AND property = '*' AND principalId = 'employee';
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', '*', '*', 'ALLOW', 'ROLE', 'itManagement'),
('VnUser', '__get__preview', 'READ', 'ALLOW', 'ROLE', 'employee'),
('VnUser', 'preview', '*', 'ALLOW', 'ROLE', 'employee'),
('VnUser', 'create', '*', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'renewToken', 'WRITE', 'ALLOW', 'ROLE', 'employee')

View File

@ -0,0 +1,40 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`entry_updateComission`(vCurrency INT)
BEGIN
/**
* Actualiza la comision de las entradas de hoy a futuro y las recalcula
*
* @param vCurrency id del tipo de moneda(SAR,EUR,USD,GBP,JPY)
*/
DECLARE vCurrencyName VARCHAR(25);
DECLARE vComission INT;
CREATE OR REPLACE TEMPORARY TABLE tmp.recalcEntryCommision
SELECT e.id
FROM vn.entry e
JOIN vn.travel t ON t.id = e.travelFk
JOIN vn.warehouse w ON w.id = t.warehouseInFk
WHERE t.shipped >= util.VN_CURDATE()
AND e.currencyFk = vCurrency;
SET vComission = currency_getCommission(vCurrency);
UPDATE vn.entry e
JOIN tmp.recalcEntryCommision tmp ON tmp.id = e.id
SET e.commission = vComission;
SELECT `name` INTO vCurrencyName
FROM currency
WHERE id = vCurrency;
CALL entry_recalc();
SELECT util.notification_send(
'entry-update-comission',
JSON_OBJECT('currencyName', vCurrencyName, 'referenceCurrent', vComission),
account.myUser_getId()
);
DROP TEMPORARY TABLE tmp.recalcEntryCommision;
END$$
DELIMITER ;

View File

@ -0,0 +1,71 @@
CREATE TABLE `vn`.`packingSiteAdvanced` (
`ticketFk` int(11),
`workerFk` int(10) unsigned,
PRIMARY KEY (`ticketFk`),
KEY `packingSiteAdvanced_FK_1` (`workerFk`),
CONSTRAINT `packingSiteAdvanced_FK` FOREIGN KEY (`ticketFk`) REFERENCES `ticket` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `packingSiteAdvanced_FK_1` FOREIGN KEY (`workerFk`) REFERENCES `worker` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('PackingSiteAdvanced', '*', '*', 'ALLOW', 'ROLE', 'production');
DROP PROCEDURE IF EXISTS `vn`.`packingSite_startCollection`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`packingSite_startCollection`(vSelf INT, vTicketFk INT)
proc: BEGIN
/**
* @param vSelf packingSite id
* @param vTicketFk A ticket id from the collection to start
*/
DECLARE vExists BOOL;
DECLARE vIsAdvanced BOOL;
DECLARE vNewCollectionFk INT;
DECLARE vOldCollectionFk INT;
DECLARE vIsPackingByOther BOOL;
SELECT id, collectionFk
INTO vExists, vOldCollectionFk
FROM packingSite
WHERE id = vSelf;
IF NOT vExists THEN
CALL util.throw('packingSiteNotExists');
END IF;
SELECT COUNT(*) > 0
INTO vIsAdvanced
FROM packingSiteAdvanced
WHERE ticketFk = vTicketFk;
IF vIsAdvanced THEN
LEAVE proc;
END IF;
SELECT collectionFk INTO vNewCollectionFk
FROM ticketCollection WHERE ticketFk = vTicketFk;
IF vOldCollectionFk IS NOT NULL
AND vOldCollectionFk <> vNewCollectionFk THEN
SELECT COUNT(*) > 0
INTO vIsPackingByOther
FROM packingSite
WHERE id <> vSelf
AND collectionFk = vOldCollectionFk;
IF NOT vIsPackingByOther AND NOT collection_isPacked(vOldCollectionFk) THEN
CALL util.throw('cannotChangeCollection');
END IF;
END IF;
UPDATE packingSite SET collectionFk = vNewCollectionFk
WHERE id = vSelf;
END$$
DELIMITER ;

View File

@ -0,0 +1,10 @@
CREATE TABLE `salix`.`accessTokenConfig` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`renewPeriod` int(10) unsigned DEFAULT NULL,
`renewInterval` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT IGNORE INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `renewInterval`)
VALUES
(1, 21600, 300);

View File

@ -0,0 +1,13 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES
('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'),
('InvoiceOutConfig','*','READ','ALLOW','ROLE','invoicing');
CREATE OR REPLACE TABLE vn.invoiceOutConfig (
id INT UNSIGNED auto_increment NOT NULL,
parallelism int UNSIGNED DEFAULT 1 NOT NULL,
PRIMARY KEY (id)
)
ENGINE=InnoDB
DEFAULT CHARSET=utf8mb3
COLLATE=utf8mb3_unicode_ci;

View File

View File

@ -5,3 +5,6 @@ CMD=mysqld
docker_setup_env "$CMD"
docker_temp_server_start "$CMD"
bash import-changes.sh
docker-dump.sh fixtures
docker_temp_server_stop

7
db/docker/docker-structure.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
. docker-init.sh
docker-dump.sh structure
docker-dump.sh mockDate
docker-dump.sh dumpedFixtures
. docker-temp-stop.sh

0
db/docker/docker-temp-stop.sh Executable file → Normal file
View File

View File

@ -782,6 +782,38 @@ USE `sage`;
-- Dumping data for table `TiposIva`
--
LOCK TABLES `taxType` WRITE;
/*!40000 ALTER TABLE `taxType` DISABLE KEYS */;
INSERT INTO `sage`.`taxType` (id, code, isIntracommunity) VALUES
(2, NULL, 0),
(4, 'national4', 0),
(5, NULL, 0),
(6, NULL, 1),
(7, NULL, 1),
(8, NULL, 1),
(10, 'national10', 0),
(11, NULL, 0),
(16, 'CEEServices21', 1),
(18, NULL, 0),
(20, 'national0', 0),
(21, 'national21', 0),
(22, 'import10', 0),
(26, NULL, 0),
(90, 'import21', 0),
(91, NULL, 0),
(92, NULL, 0),
(93, NULL, 0),
(94, NULL, 0),
(100, NULL, 0),
(108, NULL, 0),
(109, NULL, 0),
(110, NULL, 1),
(111, NULL, 0),
(112, NULL, 0),
(113, 'ISP21', 0),
(114, NULL, 0),
(115, 'import4', 0);
LOCK TABLES `TiposIva` WRITE;
/*!40000 ALTER TABLE `TiposIva` DISABLE KEYS */;
INSERT INTO `TiposIva` VALUES (2,0,'Operaciones no sujetas',0.0000000000,0.0000000000,0.0000000000,'','4770000020','','','','','','','95B21A93-5910-489D-83BB-C32788C9B19D','','','','','','','','','',0,0),(4,0,'I.V.A. 4%',0.0000000000,4.0000000000,0.0000000000,'4720000004','4770000004','','6310000000','','','','','9E6160D5-984E-4643-ACBC-1EBC3BF73360','','','','','','','','','',0,0),(5,0,'I.V.A. 4% y R.E. 0.5%',0.0000000000,4.0000000000,0.5000000000,'','4770000504','4770000405','','','','','','DBEFA562-63FB-4FFC-8171-64F0C6F065FF','','','','','','','','','',0,0),(6,0,'H.P. IVA 4% CEE',0.0000000000,4.0000000000,0.0000000000,'4721000004','4771000004','','','','','','','DD0ECBA8-2EF5-425E-911B-623580BADA77','','','','','','','','','',0,1),(7,0,'H.P. IVA 10% CEE',0.0000000000,10.0000000000,0.0000000000,'4721000011','4771000010','','','','','','','593208CD-6F28-4489-B6EC-907AD689EAC9','','','','','','','','','',0,1),(8,0,'H.P. IVA 21% CEE',0.0000000000,21.0000000000,0.0000000000,'4721000021','4771000021','','','','','','','27061852-9BC1-4C4F-9B6E-69970E208F23','','','','','','','','','',0,1),(10,0,'I.V.A. 10% Nacional',0.0000000000,10.0000000000,0.0000000000,'4720000011','4770000010','','6290000553','','','','','828A9D6F-5C01-4C3A-918A-B2E4482830D3','','','','','','','','','',0,0),(11,0,'I.V.A. 10% y R.E. 1,4%',0.0000000000,10.0000000000,1.4000000000,'','4770000101','4770000110','','','','','','C1F2D910-83A1-4191-A76C-8B3D7AB98348','','','','','','','','','',0,0),(16,0,'I.V.A. Adqui. servicios CEE',0.0000000000,21.0000000000,0.0000000000,'4721000015','4771000016','','','','','','','E3EDE961-CE8F-41D4-9E6C-D8BCD32275A1','','','','','','','','','',0,1),(18,0,'H.P. Iva Importación 0% ISP',0.0000000000,0.0000000000,0.0000000000,'4720000005','4770000005','','','','','','','27AD4158-2349-49C2-B53A-A4E0EFAC5D09','','','','','','','','','',0,0),(20,0,'I.V.A 0% Nacional',0.0000000000,0.0000000000,0.0000000000,'4720000000','','','','','','','','B90B0FBD-E513-4F04-9721-C873504E08DF','','','','','','','','','',0,0),(21,0,'I.V.A. 21%',0.0000000000,21.0000000000,0.0000000000,'4720000021','4770000021','4770000000','','','','','','BA8C4E28-DCFA-4F7B-AE4F-CA044626B55E','','','','','','','','','',0,0),(22,0,'IVA 10% importaciones',0.0000000000,10.0000000000,0.0000000000,'4722000010','','','','','','','','540450A8-4B41-4607-96D1-E7F296FB6933','','','','','','','','','',0,0),(26,0,'I.V.A. 21% y R.E. 5,2%',0.0000000000,21.0000000000,5.2000000000,'4720000021','4770000215','4770000521','631000000','','','','','2BC0765F-7739-49AE-A5F0-28B648B81677','','','','','','','','','',0,0),(90,0,'IVA 21% importaciones',0.0000000000,21.0000000000,0.0000000000,'4722000021','','','','','','','','EB675F91-5FF2-4E26-A31E-EEB674125945','','','','','','','','','',0,0),(91,0,'IVA 0% importaciones',0.0000000000,0.0000000000,0.0000000000,'4723000000','','','','','','','','5E5EFA56-2A99-4D54-A16B-5D818274CA18','','','','','','','','','',0,0),(92,0,'8.5% comp. ganadera o pesquera',0.0000000000,8.5000000000,0.0000000000,'4720000000','4770000000','477000000','631000000','','','','','','','','','','','','','','',0,0),(93,0,'12% com. agrícola o forestal',0.0000000000,12.0000000000,0.0000000000,'4720000012','','','','','','','','267B1DDB-247F-4A71-AB95-3349FEFC5F92','','','','','','','','','',0,0),(94,0,'10,5% com. ganadera o pesquera',0.0000000000,10.5000000000,0.0000000000,'4770000000','4720000000','631000000','477000000','','','','','','','','','','','','','','',0,0),(100,0,'HP IVA SOPORTADO 5%',0.0000000000,5.0000000000,0.0000000000,'4720000055','','','','','','','','3AD36CB2-4172-4CC9-9F87-2BF2B56AAC80','','','','','','','','','',0,0),(108,0,'I.V.A. 8%',0.0000000000,8.0000000000,0.0000000000,'4720000000','4770000000','477000000','631000000','','','','','','','','','','','','','','',0,0),(109,0,'I.V.A. 8% y R.E. 1%',0.0000000000,8.0000000000,1.0000000000,'4720000000','4770000000','477000000','631000000','','','','','','','','','','','','','','',0,0),(110,0,'HP IVA Devengado Exento CEE',0.0000000000,0.0000000000,0.0000000000,'','4771000000','','','','','','','C605BC32-E161-42FD-83F3-3A66B1FBE399','','','','','','','','','',0,1),(111,0,'H.P. Iva Devengado Exento Ser',0.0000000000,0.0000000000,0.0000000000,'','4771000001','','','','','','','F1AEC4DC-AFE5-498E-A713-2648FFB6DA32','','','','','','','','','',0,0),(112,0,'H.P. IVA Devengado en exportac',0.0000000000,0.0000000000,0.0000000000,'','4770000002','','','','','','','F980AE74-BF75-4F4C-927F-0CCCE0DB8D15','','','','','','','','','',0,0),(113,0,'HP DEVENGADO 21 ISP ',0.0000000000,21.0000000000,0.0000000000,'4720000006','4770000006','','','','','','','728D7A76-E936-438C-AF05-3CA38FE16EA5','','','','','','','','','',0,0),(114,0,'HP.IVA NO DEDUCIBLE 10%',0.0000000000,0.0000000000,0.0000000000,'4720000026','','','','','','','','','','','','','','','','','',0,0),(115,0,'H.P. IVA Soportado Impor 4% ',0.0000000000,4.0000000000,0.0000000000,'4722000004','','','','','','','','','','','','','','','','','',0,0);

View File

@ -71,8 +71,8 @@ INSERT INTO `account`.`roleConfig`(`id`, `mysqlPassword`, `rolePrefix`, `userPre
CALL `account`.`role_sync`;
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`, `image`, `bcryptPassword`)
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2'
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `lang`, `image`, `password`)
SELECT id, name, CONCAT(name, 'Nick'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2'
FROM `account`.`role` WHERE id <> 20
ORDER BY id;
@ -98,20 +98,24 @@ INSERT INTO `hedera`.`tpvConfig`(`id`, `currency`, `terminal`, `transactionType`
VALUES
(1, 978, 1, 0, 2000, 9, 0);
INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `bcryptPassword`, `password`,`role`,`active`,`email`,`lang`, `image`)
INSERT INTO `account`.`user`(`id`,`name`,`nickname`, `password`,`role`,`active`,`email`,`lang`, `image`)
VALUES
(1101, 'BruceWayne', 'Bruce Wayne', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1102, 'PetterParker', 'Petter Parker', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'PetterParker@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1103, 'ClarkKent', 'Clark Kent', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'ClarkKent@mydomain.com', 'fr', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1104, 'TonyStark', 'Tony Stark', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'TonyStark@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1105, 'MaxEisenhardt', 'Max Eisenhardt', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'MaxEisenhardt@mydomain.com', 'pt', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1106, 'DavidCharlesHaller', 'David Charles Haller', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'DavidCharlesHaller@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1107, 'HankPym', 'Hank Pym', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'HankPym@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1108, 'CharlesXavier', 'Charles Xavier', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'CharlesXavier@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1109, 'BruceBanner', 'Bruce Banner', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'BruceBanner@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1110, 'JessicaJones', 'Jessica Jones', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 1, 1, 'JessicaJones@mydomain.com', 'en', NULL),
(1111, 'Missing', 'Missing', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en', NULL),
(1112, 'Trash', 'Trash', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'en', NULL);
(1101, 'BruceWayne', 'Bruce Wayne', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 1, 'BruceWayne@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1102, 'PetterParker', 'Petter Parker', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 1, 'PetterParker@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1103, 'ClarkKent', 'Clark Kent', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 1, 'ClarkKent@mydomain.com', 'fr', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1104, 'TonyStark', 'Tony Stark', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 1, 'TonyStark@mydomain.com', 'es', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1105, 'MaxEisenhardt', 'Max Eisenhardt', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 1, 'MaxEisenhardt@mydomain.com', 'pt', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1106, 'DavidCharlesHaller', 'David Charles Haller', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1, 1, 'DavidCharlesHaller@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1107, 'HankPym', 'Hank Pym', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1, 1, 'HankPym@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1108, 'CharlesXavier', 'Charles Xavier', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1, 1, 'CharlesXavier@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1109, 'BruceBanner', 'Bruce Banner', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1, 1, 'BruceBanner@mydomain.com', 'en', 'e7723f0b24ff05b32ed09d95196f2f29'),
(1110, 'JessicaJones', 'Jessica Jones', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 1, 1, 'JessicaJones@mydomain.com', 'en', NULL),
(1111, 'Missing', 'Missing', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 0, NULL, 'en', NULL),
(1112, 'Trash', 'Trash', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2', 2, 0, NULL, 'en', NULL);
UPDATE account.`user`
SET passExpired = DATE_SUB(util.VN_CURDATE(), INTERVAL 1 YEAR)
WHERE name = 'maintenance';
INSERT INTO `account`.`mailAlias`(`id`, `alias`, `description`, `isPublic`)
VALUES
@ -599,6 +603,9 @@ UPDATE `vn`.`invoiceOut` SET ref = 'T3333333' WHERE id = 3;
UPDATE `vn`.`invoiceOut` SET ref = 'T4444444' WHERE id = 4;
UPDATE `vn`.`invoiceOut` SET ref = 'A1111111' WHERE id = 5;
INSERT INTO vn.invoiceOutConfig
SET parallelism = 8;
INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`)
VALUES
(1, 895.76, 89.58, 4722000010),
@ -2732,7 +2739,8 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
(1, 'print-email', 'notification fixture one'),
(2, 'invoice-electronic', 'A electronic invoice has been generated'),
(3, 'not-main-printer-configured', 'A printer distinct than main has been configured'),
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated');
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated'),
(5, 'modified-entry', 'An entry has been modified');
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
VALUES
@ -2786,7 +2794,9 @@ INSERT INTO `vn`.`ticketLog` (`originFk`, userFk, `action`, changedModel, oldIns
(7, 18, 'update', 'Sale', '{"quantity":1}', '{"quantity":10}', 1, NULL),
(7, 18, 'update', 'Ticket', '{"quantity":1,"concept":"Chest ammo box"}', '{"quantity":10,"concept":"Chest ammo box"}', 1, NULL),
(7, 18, 'update', 'Sale', '{"price":3}', '{"price":5}', 1, NULL),
(7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 1x0.5m de '5' a '10'");
(7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 1x0.5m de '5' a '10'"),
(16, 9, 'update', 'Sale', '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', '{"quantity":8,"concept":"Shield", "price": 10.5, "itemFk": 1}' , 5689, 'Shield');
INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedModel, changedModelId, changedModelValue, oldInstance, newInstance, description)
VALUES
@ -2800,7 +2810,6 @@ INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedM
(1, 18, 'select', '2000-12-27 03:40:30', 'Ticket', 45, NULL , NULL, NULL, NULL),
(1, 18, 'insert', '2000-04-10 09:40:15', 'Sale', 5689, 'Shield' , NULL, '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', NULL),
(1, 18, 'insert', '1999-05-09 10:00:00', 'Ticket', 45, 'Super Man' , NULL, '{"id":45,"clientFk":8608,"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","addressFk":48637,"isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"created":"2023-05-16T11:42:56.000Z","isBlocked":false,"hasPriority":false,"companyFk":442,"agencyModeFk":639,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}', NULL);
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', '1,6', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
@ -2889,6 +2898,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
(2, 1, 50, 2),
(3, 1, 0, 3);
INSERT INTO `salix`.`accessTokenConfig` (`id`, `renewPeriod`, `renewInterval`)
VALUES
(1, 21600, 300);
INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agencyFk`, `companyFk`)
VALUES
(1, 1, 1, 1, 442);
@ -2896,3 +2909,10 @@ INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agenc
INSERT INTO `vn`.`buyConfig` (`id`, `monthsAgo`)
VALUES
(1, 6);
INSERT INTO `vn`.`invoiceInSerial` (`code`, `description`, `cplusTerIdNifFk`, `taxAreaFk`)
VALUES
('C', 'Asgard', 1, 'WORLD'),
('E', 'Midgard', 1, 'CEE'),
('R', 'Jotunheim', 1, 'NATIONAL'),
('W', 'Vanaheim', 1, 'WORLD');

View File

@ -1,30 +1,19 @@
DROP FUNCTION IF EXISTS `util`.`mockTime`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` FUNCTION `util`.`mockTime`() RETURNS datetime
DETERMINISTIC
BEGIN
RETURN CONVERT_TZ('@mockDate', 'utc', 'Europe/Madrid');
END$$
DELIMITER ;
DROP FUNCTION IF EXISTS `util`.`mockUtcTime`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` FUNCTION `util`.`mockUtcTime`() RETURNS datetime
DETERMINISTIC
BEGIN
RETURN CONVERT_TZ('@mockDate', 'utc', 'Europe/Madrid');
END$$
DELIMITER ;
DROP FUNCTION IF EXISTS `util`.`mockTimeBase`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` FUNCTION `util`.`mockTimeBase`(vIsUtc BOOL) RETURNS datetime
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `util`.`mockTime`() RETURNS datetime
DETERMINISTIC
BEGIN
RETURN CONVERT_TZ('@mockDate', 'utc', 'Europe/Madrid');
END$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `util`.`mockUtcTime`() RETURNS datetime
DETERMINISTIC
BEGIN
RETURN CONVERT_TZ('@mockDate', 'utc', 'Europe/Madrid');
END$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `util`.`mockTimeBase`(vIsUtc BOOL) RETURNS datetime
DETERMINISTIC
BEGIN
RETURN CONVERT_TZ('@mockDate', 'utc', 'Europe/Madrid');

View File

@ -1,4 +0,0 @@
-- Import compiled functions
CREATE AGGREGATE FUNCTION minacum RETURNS INT SONAME 'minacum.so';
CREATE AGGREGATE FUNCTION multimax RETURNS INT SONAME 'multimax.so';

View File

@ -15620,6 +15620,18 @@ CREATE TABLE `ClavesOperacion` (
--
-- Table structure for table `Municipios`
--
DROP TABLE IF EXISTS `taxType`;
CREATE TABLE `taxType` (
id INT(11) NOT NULL,
code VARCHAR(25) DEFAULT NULL NULL,
isIntracommunity TINYINT(1) DEFAULT FALSE NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `taxType_UN` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='Coincidencia del id con Sage.TiposIVA.CodigoIva(propia de Sage), en ningún caso vincular mediate FK';
ALTER TABLE `sage`.`taxType` ADD CONSTRAINT taxType_PK PRIMARY KEY IF NOT EXISTS (id);
ALTER TABLE `sage`.`taxType` ADD CONSTRAINT taxType_UN UNIQUE KEY IF NOT EXISTS (code);
DROP TABLE IF EXISTS `Municipios`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
@ -22074,12 +22086,14 @@ CREATE TABLE `autonomy` (
`name` varchar(100) NOT NULL,
`countryFk` mediumint(8) unsigned NOT NULL,
`geoFk` int(11) DEFAULT NULL,
`isUeeMember` tinyint(1) DEFAULT NULL,
`hasDailyInvoice` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `autonomy_FK` (`countryFk`),
KEY `autonomy_FK_1` (`geoFk`),
CONSTRAINT `autonomy_FK` FOREIGN KEY (`countryFk`) REFERENCES `country` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `autonomy_FK_1` FOREIGN KEY (`geoFk`) REFERENCES `zoneGeo` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDBDEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci COMMENT='Comunidades autónomas o su equivalente en otros paises. Agrupación de provincias, en una categoria inferior a country.';
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci COMMENT='Comunidades autónomas o su equivalente en otros paises. Agrupación de provincias, en una categoria inferior a country.';
/*!40101 SET character_set_client = @saved_cs_client */;
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
@ -28805,7 +28819,10 @@ CREATE TABLE `expence` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
ALTER TABLE `vn`.`expence`
ADD code VARCHAR(25) DEFAULT NULL NULL;
ALTER TABLE `vn`.`expence`
ADD CONSTRAINT expence_UN UNIQUE KEY IF NOT EXISTS (code);
--
-- Table structure for table `farming`
--
@ -57317,7 +57334,7 @@ DELIMITER ;
/*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ;
/*!50003 SET collation_connection = @saved_col_connection */ ;
/*!50003 DROP PROCEDURE IF EXISTS `invoiceInBookingMain` */;
/*!50003 DROP PROCEDURE IF EXISTS `invoiceIn_booking` */;
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
@ -57326,28 +57343,71 @@ DELIMITER ;
/*!50003 SET collation_connection = utf8mb4_general_ci */ ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `invoiceInBookingMain`(vInvoiceInId INT)
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`invoiceIn_booking`(vSelf INT)
BEGIN
DECLARE vTotalAmount,vTotalAmountDivisa DECIMAL(10,2);
DECLARE vBookNumber,vSerialNumber INT;
DECLARE vRate DECIMAL(10,4);
DECLARE vBookNumber INT;
CALL invoiceInBookingCommon(vInvoiceInId,vSerialNumber);
SELECT SUM(iit.taxableBase * IF( i.serial= 'R' AND ti.Iva <> 'HP DEVENGADO 21 ISP', 1 +(ti.PorcentajeIva/100),1)),
SUM(iit.foreignValue * IF( i.serial= 'R', 1 + (ti.PorcentajeIva/100),1)),
iit.taxableBase/iit.foreignValue
INTO vTotalAmount, vTotalAmountDivisa, vRate
FROM newInvoiceIn i
JOIN invoiceInTax iit ON iit.invoiceInFk = i.id
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk;
DROP TEMPORARY TABLE IF EXISTS tInvoiceIn;
CREATE TEMPORARY TABLE tInvoiceIn
ENGINE = MEMORY
SELECT ii.bookEntried,
iit.foreignValue,
ii.companyFk,
ii.expenceFkDeductible,
iit.taxableBase,
ii.serial,
ii.issued,
ii.operated,
ii.supplierRef,
ii.cplusTrascendency472Fk,
ii.cplusTaxBreakFk,
ii.cplusSubjectOpFk,
ii.cplusInvoiceType472Fk,
ii.cplusRectificationTypeFk,
ii.booked,
IFNULL(a.isUeeMember, c.isUeeMember) isUeeMember,
(c.id = cc.id) isSameCountry,
s.account supplierAccount,
s.name supplierName,
s.nif,
iit.taxTypeSageFk,
tt.code taxCode,
ti.Iva,
ti.CuentaIvaSoportado,
ti.PorcentajeIva,
ti.CuentaIvaRepercutido,
ttr.ClaveOperacionDefecto,
iis.cplusTerIdNifFk,
cit.id invoicesCount,
e.code,
e.isWithheld,
e.id expenceFk,
e.name expenceName
FROM invoiceIn ii
JOIN supplier s ON s.id = ii.supplierFk
LEFT JOIN province p ON p.id = s.provinceFk
LEFT JOIN autonomy a ON a.id = p.autonomyFk
JOIN country c ON c.id = s.countryFk
JOIN supplier sc ON sc.id = ii.companyFk
JOIN country cc ON cc.id = sc.countryFk
JOIN invoiceInSerial iis ON iis.code = ii.serial
JOIN cplusInvoiceType472 cit ON cit.id = ii.cplusInvoiceType472Fk
LEFT JOIN invoiceInTax iit ON iit.invoiceInFk = ii.id
LEFT JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = iit.transactionTypeSageFk
LEFT JOIN expence e ON e.id = iit.expenceFk
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
LEFT JOIN sage.taxType tt ON tt.id = ti.CodigoIva
WHERE ii.id = vSelf;
CALL vn.ledger_next(vBookNumber);
-- Apunte del proveedor
INSERT INTO XDiario(ASIEN,
INSERT INTO XDiario(
ASIEN,
FECHA,
SUBCTA,
EUROHABER,
@ -57356,24 +57416,30 @@ BEGIN
HABERME,
NFACTICK,
CLAVE,
empresa_id
)
empresa_id)
SELECT
vBookNumber,
n.bookEntried,
s.supplierAccount,
vTotalAmount EUROHABER,
n.conceptWithSupplier,
vRate,
vTotalAmountDivisa,
n.invoicesCount,
vInvoiceInId,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s;
vBookNumber ASIEN,
tii.bookEntried FECHA,
tii.supplierAccount SUBCTA,
SUM(tii.taxableBase *
IF(tii.serial= 'R' AND ((tii.taxCode IS NULL OR tii.taxCode <> 'ISP21')
AND tii.taxTypeSageFk IS NOT NULL),
1 + (tii.PorcentajeIva / 100),
1)) EUROHABER,
CONCAT('s/fra',
RIGHT(tii.supplierRef, 8),
':',
LEFT(tii.supplierName, 10)) CONCEPTO,
CAST(tii.taxableBase / tii.foreignValue AS DECIMAL (10,4)) CAMBIO,
SUM(tii.foreignValue * IF(tii.serial = 'R', 1 + (tii.PorcentajeIva / 100), 1)) HABERME,
tii.invoicesCount NFACTICK,
vSelf CLAVE,
tii.companyFk empresa_id
FROM tInvoiceIn tii;
-- Línea de Gastos
INSERT INTO XDiario ( ASIEN,
INSERT INTO XDiario(
ASIEN,
FECHA,
SUBCTA,
CONTRA,
@ -57384,30 +57450,29 @@ BEGIN
DEBEME,
HABERME,
NFACTICK,
empresa_id
)
empresa_id)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(e.isWithheld , LPAD(RIGHT(s.supplierAccount,5),10,iit.expenceFk),iit.expenceFk) SUBCTA,
s.supplierAccount CONTRA,
IF(e.isWithheld AND iit.taxableBase < 0, NULL, ROUND(SUM(iit.taxableBase),2)) EURODEBE,
IF(e.isWithheld AND iit.taxableBase < 0,ROUND(SUM(-iit.taxableBase),2),NULL) EUROHABER,
n.conceptWithSupplier CONCEPTO,
vRate,
IF(e.isWithheld,NULL,ABS(ROUND(SUM(iit.foreignValue),2))) DEBEME,
IF(e.isWithheld,ABS(ROUND(SUM(iit.foreignValue),2)),NULL) HABERME,
n.invoicesCount NFACTICK,
n.companyFk empresa_id
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax iit ON iit.invoiceInFk = n.id
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = iit.expenceFk
WHERE e.name != 'Suplidos Transitarios nacionales'
GROUP BY iit.expenceFk;
tii.bookEntried FECHA,
IF(tii.isWithheld, LPAD(RIGHT(tii.supplierAccount, 5), 10, tii.expenceFk),tii.expenceFk) SUBCTA,
tii.supplierAccount CONTRA,
IF(tii.isWithheld AND tii.taxableBase < 0, NULL, ROUND(SUM(tii.taxableBase),2)) EURODEBE,
IF(tii.isWithheld AND tii.taxableBase < 0, ROUND(SUM(-tii.taxableBase), 2), NULL) EUROHABER,
CONCAT('s/fra',
RIGHT(tii.supplierRef, 8),
':',
LEFT(tii.supplierName, 10)) CONCEPTO,
CAST(tii.taxableBase / tii.foreignValue AS DECIMAL (10, 4)) CAMBIO,
IF(tii.isWithheld, NULL,ABS(ROUND(SUM(tii.foreignValue), 2))) DEBEME,
IF(tii.isWithheld, ABS(ROUND(SUM(tii.foreignValue), 2)) ,NULL) HABERME,
tii.invoicesCount NFACTICK,
tii.companyFk empresa_id
FROM tInvoiceIn tii
WHERE tii.code IS NULL OR tii.code <> 'suplido'
GROUP BY tii.expenceFk;
-- Líneas de IVA
INSERT INTO XDiario( ASIEN,
INSERT INTO XDiario(
ASIEN,
FECHA,
SUBCTA,
CONTRA,
@ -57434,56 +57499,50 @@ BEGIN
TERNIF,
TERNOM,
FECREGCON,
empresa_id
)
empresa_id)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(n.expenceFkDeductible>0, n.expenceFkDeductible, ti.CuentaIvaSoportado) SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100 /* + 0.0001*/ , 2)) EURODEBE,
SUM(it.taxableBase) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
IF(isUeeMember AND eWithheld.id IS NULL,'','*') AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
tii.bookEntried FECHA,
IF(tii.expenceFkDeductible>0, tii.expenceFkDeductible, tii.CuentaIvaSoportado) SUBCTA,
tii.supplierAccount CONTRA,
SUM(ROUND(tii.PorcentajeIva * tii.taxableBase / 100, 2)) EURODEBE,
SUM(tii.taxableBase) BASEEURO,
GROUP_CONCAT(DISTINCT tii.expenceName SEPARATOR ', ') CONCEPTO,
vSelf FACTURA,
tii.PorcentajeIva IVA,
IF(tii.isUeeMember AND eWithheld.id IS NULL, '', '*') AUXILIAR,
tii.serial SERIE,
tii.ClaveOperacionDefecto,
tii.issued FECHA_EX,
tii.operated FECHA_OP,
tii.invoicesCount NFACTICK,
tii.supplierRef FACTURAEX,
TRUE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
n.cplusTrascendency472Fk TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.booked FECREGCON,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
(tii.isSameCountry OR NOT tii.isUeeMember) LRECT349,
tii.cplusTrascendency472Fk TIPOCLAVE,
tii.cplusTaxBreakFk TIPOEXENCI,
tii.cplusSubjectOpFk TIPONOSUJE,
tii.cplusInvoiceType472Fk TIPOFACT,
tii.cplusRectificationTypeFk TIPORECTIF,
tii.cplusTerIdNifFk TERIDNIF,
tii.nif TERNIF,
tii.supplierName TERNOM,
tii.booked FECREGCON,
tii.companyFk
FROM tInvoiceIn tii
LEFT JOIN (
SELECT eWithheld.id
FROM invoiceInTax hold
JOIN expence eWithheld ON eWithheld.id = hold.expenceFk AND eWithheld.isWithheld
WHERE hold.invoiceInFk = vInvoiceInId LIMIT 1
SELECT e.id
FROM tInvoiceIn tii
JOIN expence e ON e.id = tii.expenceFk
WHERE e.isWithheld
LIMIT 1
) eWithheld ON TRUE
WHERE it.taxTypeSageFk IS NOT NULL
AND it.taxTypeSageFk NOT IN (22, 90)
GROUP BY ti.PorcentajeIva, e.id;
WHERE tii.taxTypeSageFk IS NOT NULL
AND (tii.taxCode IS NULL OR tii.taxCode NOT IN ('import10', 'import21'))
GROUP BY tii.PorcentajeIva, tii.expenceFk;
-- Línea iva inversor sujeto pasivo
INSERT INTO XDiario( ASIEN,
INSERT INTO XDiario(
ASIEN,
FECHA,
SUBCTA,
CONTRA,
@ -57509,50 +57568,43 @@ BEGIN
TERIDNIF,
TERNIF,
TERNOM,
empresa_id
)
empresa_id)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
ti.CuentaIvaRepercutido SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100,2)) EUROHABER,
ROUND(SUM(it.taxableBase),2) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
tii.bookEntried FECHA,
tii.CuentaIvaRepercutido SUBCTA,
tii.supplierAccount CONTRA,
SUM(ROUND(tii.PorcentajeIva * tii.taxableBase / 100,2)) EUROHABER,
ROUND(SUM(tii.taxableBase),2) BASEEURO,
GROUP_CONCAT(DISTINCT tii.expenceName SEPARATOR ', ') CONCEPTO,
vSelf FACTURA,
tii.PorcentajeIva IVA,
'*' AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
tii.serial SERIE,
tii.ClaveOperacionDefecto,
tii.issued FECHA_EX,
tii.operated FECHA_OP,
tii.invoicesCount NFACTICK,
tii.supplierRef FACTURAEX,
FALSE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
(tii.isSameCountry OR NOT tii.isUeeMember) LRECT349,
1 TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
WHERE ti.Iva = 'HP DEVENGADO 21 ISP' OR MID(s.account, 4, 1) = '1'
GROUP BY ti.PorcentajeIva, e.id;
tii.cplusTaxBreakFk TIPOEXENCI,
tii.cplusSubjectOpFk TIPONOSUJE,
tii.cplusInvoiceType472Fk TIPOFACT,
tii.cplusRectificationTypeFk TIPORECTIF,
tii.cplusTerIdNifFk TERIDNIF,
tii.nif TERNIF,
tii.supplierName TERNOM,
tii.companyFk
FROM tInvoiceIn tii
WHERE tii.taxCode = 'ISP21' OR MID(tii.supplierAccount, 4, 1) = '1'
AND tii.taxTypeSageFk IS NOT NULL
GROUP BY tii.PorcentajeIva, tii.expenceFk;
-- Actualización del registro original
UPDATE invoiceIn ii
JOIN newInvoiceIn ni ON ii.id = ni.id
SET ii.serialNumber = vSerialNumber,
ii.isBooked = TRUE;
SET ii.isBooked = TRUE
WHERE ii.id = vSelf;
-- Problemas derivados de la precisión en los decimales al calcular los impuestos
UPDATE XDiario
@ -57569,8 +57621,12 @@ BEGIN
ORDER BY id DESC
LIMIT 1;
DROP TEMPORARY TABLE tInvoiceIn;
END ;;
DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ;
/*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ;

View File

@ -110,5 +110,6 @@ TABLES=(
TiposIva
TiposTransacciones
TiposRetencion
taxType
)
dump_tables ${TABLES[@]}

View File

@ -311,9 +311,9 @@ export default {
},
clientDefaulter: {
anyClient: 'vn-client-defaulter tbody > tr',
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(2) > span',
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(3) > span',
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(1) > td:nth-child(8) > vn-textarea[ng-model="defaulter.observation"]',
firstClientName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(2) > span',
firstSalesPersonName: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(3) > span',
firstObservation: 'vn-client-defaulter tbody > tr:nth-child(2) > td:nth-child(8) > vn-textarea[ng-model="defaulter.observation"]',
allDefaulterCheckbox: 'vn-client-defaulter thead vn-multi-check',
addObservationButton: 'vn-client-defaulter vn-button[icon="icon-notes"]',
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
@ -334,15 +334,15 @@ export default {
},
itemsIndex: {
createItemButton: `vn-float-button`,
firstSearchResult: 'vn-item-index tbody tr:nth-child(1)',
firstSearchResult: 'vn-item-index tbody tr:nth-child(2)',
searchResult: 'vn-item-index tbody tr:not(.empty-rows)',
firstResultPreviewButton: 'vn-item-index tbody > :nth-child(1) .buttons > [icon="preview"]',
firstResultPreviewButton: 'vn-item-index tbody > :nth-child(2) .buttons > [icon="preview"]',
searchResultCloneButton: 'vn-item-index .buttons > [icon="icon-clone"]',
acceptClonationAlertButton: '.vn-confirm.shown [response="accept"]',
closeItemSummaryPreview: '.vn-popup.shown',
shownColumns: 'vn-item-index vn-button[id="shownColumns"]',
shownColumnsList: '.vn-popover.shown .content',
firstItemImage: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(1) > img',
firstItemImage: 'vn-item-index tbody > tr:nth-child(2) > td:nth-child(1) > img',
firstItemImageTd: 'vn-item-index smart-table tr:nth-child(1) td:nth-child(1)',
firstItemId: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(2)',
idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Identifier"]',
@ -479,9 +479,6 @@ export default {
fourthBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(4) > vn-td.balance > span',
firstBalance: 'vn-item-diary vn-tbody > vn-tr:nth-child(1) > vn-td.balance'
},
itemLog: {
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
},
ticketSummary: {
header: 'vn-ticket-summary > vn-card > h5',
state: 'vn-ticket-summary vn-label-value[label="State"] > section > span',
@ -523,11 +520,11 @@ export default {
searchResultDate: 'vn-ticket-summary [label=Landed] span',
topbarSearch: 'vn-searchbar',
moreMenu: 'vn-ticket-index vn-icon-button[icon=more_vert]',
fourthWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(4)',
fiveWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(5)',
fourthWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(5)',
fiveWeeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(6)',
weeklyTicket: 'vn-ticket-weekly-index vn-card smart-table slot-table table tbody tr',
firstWeeklyTicketDeleteIcon: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(1) vn-icon-button[icon="delete"]',
firstWeeklyTicketAgency: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(1) [ng-model="weekly.agencyModeFk"]',
firstWeeklyTicketDeleteIcon: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(2) vn-icon-button[icon="delete"]',
firstWeeklyTicketAgency: 'vn-ticket-weekly-index vn-card smart-table slot-table tr:nth-child(2) [ng-model="weekly.agencyModeFk"]',
acceptDeleteTurn: '.vn-confirm.shown button[response="accept"]'
},
createTicketView: {
@ -572,15 +569,15 @@ export default {
submitNotesButton: 'button[type=submit]'
},
ticketExpedition: {
firstSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(1) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(3) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="keyboard_arrow_down"]',
firstSaleCheckbox: 'vn-ticket-expedition tr:nth-child(2) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition tr:nth-child(4) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition slot-actions > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition slot-actions > vn-button[icon="keyboard_arrow_down"]',
moreMenuWithoutRoute: 'vn-item[name="withoutRoute"]',
moreMenuWithRoute: 'vn-item[name="withRoute"]',
newRouteId: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newRoute"]',
saveButton: '.vn-dialog.shown [response="accept"]',
expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr'
expeditionRow: 'vn-ticket-expedition table tbody > tr'
},
ticketSales: {
setOk: 'vn-ticket-sale vn-tool-bar > vn-button[label="Ok"] > button',
@ -595,6 +592,8 @@ export default {
moreMenuUpdateDiscount: 'vn-item[name="discount"]',
moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]',
moreMenuRefund: 'vn-item[name="refund"]',
refundWithWarehouse: 'vn-item[name="refundWithWarehouse"]',
refundWithoutWarehouse: 'vn-item[name="refundWithoutWarehouse"]',
moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input',
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',
@ -665,15 +664,6 @@ export default {
thirdRemoveRequestButton: 'vn-ticket-request-index vn-tr:nth-child(3) vn-icon[icon="delete"]',
thirdRequestQuantity: 'vn-ticket-request-index vn-table vn-tr:nth-child(3) > vn-td:nth-child(6) vn-input-number',
saveButton: 'vn-ticket-request-create button[type=submit]',
},
ticketLog: {
firstTD: 'vn-ticket-log vn-table vn-td:nth-child(1)',
logButton: 'vn-left-menu a[ui-sref="ticket.card.log"]',
user: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(2)',
action: 'vn-ticket-log vn-tbody vn-tr vn-td:nth-child(4)',
changes: 'vn-ticket-log vn-data-viewer vn-tbody vn-tr table tr:nth-child(2) td.after',
id: 'vn-ticket-log vn-tr:nth-child(1) table tr:nth-child(1) td.before'
},
ticketService: {
addServiceButton: 'vn-ticket-service vn-icon-button[vn-tooltip="Add service"] > button',
@ -710,7 +700,7 @@ export default {
problems: 'vn-check[label="With problems"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Future tickets"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
multiCheck: 'vn-multi-check',
tableId: 'vn-textfield[name="id"]',
tableFutureId: 'vn-textfield[name="futureId"]',
@ -734,7 +724,7 @@ export default {
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Advance tickets"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
tableId: 'vn-textfield[name="id"]',
tableFutureId: 'vn-textfield[name="futureId"]',
tableLiters: 'vn-textfield[name="liters"]',
@ -808,7 +798,7 @@ export default {
claimAction: {
importClaimButton: 'vn-claim-action vn-button[label="Import claim"]',
anyLine: 'vn-claim-action vn-tbody > vn-tr',
firstDeleteLine: 'vn-claim-action tr:nth-child(1) vn-icon-button[icon="delete"]',
firstDeleteLine: 'vn-claim-action tr:nth-child(2) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
},
ordersIndex: {
@ -1177,8 +1167,6 @@ export default {
allBuyCheckbox: 'vn-entry-buy-index thead vn-check',
firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check',
deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]',
addBuyButton: 'vn-entry-buy-index vn-icon[icon="add"]',
secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]',
secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]',
secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]',
secondBuyGrouping: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.grouping"]',
@ -1214,7 +1202,7 @@ export default {
addTagButton: 'vn-icon-button[vn-tooltip="Add tag"]',
itemTagInput: 'vn-autocomplete[ng-model="itemTag.tagFk"]',
itemTagValueInput: 'vn-autocomplete[ng-model="itemTag.value"]',
firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)',
firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(2)',
allBuysCheckBox: 'vn-entry-latest-buys thead vn-check',
secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.checked"]',
editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]',

View File

@ -19,15 +19,14 @@ describe('SmartTable SearchBar integration', () => {
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 4);
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 4);
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
@ -36,7 +35,7 @@ describe('SmartTable SearchBar integration', () => {
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 1);
});
it('should filter in section without smart-table and search in searchBar go to zone section', async() => {

View File

@ -0,0 +1,71 @@
import getBrowser from '../../helpers/puppeteer';
const $ = {
form: 'vn-out-layout form'
};
describe('ChangePassword path', async() => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
});
afterAll(async() => {
await browser.close();
});
const oldPassword = 'nightmare';
const newPassword = 'newPass.1234';
describe('Bad login', async() => {
it('should receive an error when the password is expired', async() => {
// Expired login
await page.doLogin('Maintenance', oldPassword);
let message = await page.waitForSnackbar();
expect(message.text).toContain('The password has expired, change it from Salix');
expect(await page.getState()).toContain('change-password');
// Bad attempt: incorrect current password
message = await page.sendForm($.form, {
oldPassword: newPassword,
newPassword: oldPassword,
repeatPassword: oldPassword
});
expect(message.text).toContain('Invalid current password');
// Bad attempt: password not meet requirements
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: oldPassword,
repeatPassword: oldPassword
});
expect(message.text).toContain('Password does not meet requirements');
// Correct attempt: change password
message = await page.sendForm($.form, {
oldPassword: oldPassword,
newPassword: newPassword,
repeatPassword: newPassword
});
expect(message.text).toContain('Password updated!');
expect(await page.getState()).toContain('login');
// Bad login, old password
await page.doLogin('Maintenance', oldPassword);
message = await page.waitForSnackbar();
expect(message.text).toContain('Invalid login');
// Correct login, new password
await page.doLogin('Maintenance', newPassword);
await page.waitForSelector('vn-home');
expect(await page.getState()).toBe('home');
});
});
});

View File

@ -5,8 +5,8 @@ const $ = {
userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]',
email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]',
saveButton: 'vn-client-web-access button[type=submit]',
nameValue: 'vn-client-log .change:nth-child(1) .basic-json:nth-child(2) vn-json-value',
activeValue: 'vn-client-log .change:nth-child(2) .basic-json:nth-child(1) vn-json-value'
nameValue: 'vn-client-log .changes-log:nth-child(2) .basic-json:nth-child(2) vn-json-value',
activeValue: 'vn-client-log .changes-log:nth-child(3) .basic-json:nth-child(1) vn-json-value'
};
describe('Client web access path', () => {

View File

@ -19,7 +19,7 @@ describe('Client defaulter path', () => {
it('should count the amount of clients in the turns section', async() => {
const result = await page.countElement(selectors.clientDefaulter.anyClient);
expect(result).toEqual(5);
expect(result).toEqual(6);
});
it('should check contain expected client', async() => {

View File

@ -53,7 +53,7 @@ describe('Worker create path', () => {
expect(message.text).toContain('Data saved!');
// 'rollback'
await page.loginAndModule('sysadmin', 'account');
await page.loginAndModule('itManagement', 'account');
await page.accessToSearchResult(newWorker);
await page.waitToClick(selectors.accountDescriptor.menuButton);

View File

@ -18,11 +18,11 @@ describe('Item summary path', () => {
await page.doSearch('Ranged weapon');
const resultsCount = await page.countElement(selectors.itemsIndex.searchResult);
await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Ranged weapon');
await page.waitForTextInElement(selectors.itemsIndex.firstSearchResult, 'Ranged weapon');
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
const isVisible = await page.isVisible(selectors.itemSummary.basicData);
expect(resultsCount).toBe(3);
expect(resultsCount).toBe(4);
expect(isVisible).toBeTruthy();
});
@ -66,7 +66,7 @@ describe('Item summary path', () => {
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
await page.waitForSelector(selectors.itemSummary.basicData, {visible: true});
expect(resultsCount).toBe(2);
expect(resultsCount).toBe(3);
});
it(`should now check the item summary preview shows fields from basic data`, async() => {

View File

@ -18,7 +18,7 @@ describe('Item log path', () => {
await page.doSearch('Knowledge artifact');
const nResults = await page.countElement(selectors.itemsIndex.searchResult);
expect(nResults).toEqual(0);
expect(nResults).toEqual(1);
});
it('should access to the create item view by clicking the create floating button', async() => {

View File

@ -220,14 +220,25 @@ describe('Ticket Edit sale path', () => {
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSearchResult('17');
await page.accessToSection('ticket.card.sale');
});
it('should select the third sale and create a refund', async() => {
it('should select the first sale and create a refund with warehouse', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
it('should select the first sale and create a refund without warehouse', async() => {
await page.accessToSearchResult('18');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithoutWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
@ -246,7 +257,6 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);

View File

@ -27,6 +27,6 @@ describe('Ticket expeditions and log path', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(3);
expect(result).toEqual(4);
});
});

View File

@ -19,7 +19,7 @@ describe('Ticket descriptor path', () => {
it('should count the amount of tickets in the turns section', async() => {
const result = await page.countElement(selectors.ticketsIndex.weeklyTicket);
expect(result).toEqual(6);
expect(result).toEqual(7);
});
it('should go back to the ticket index then search and access a ticket summary', async() => {
@ -89,7 +89,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch('11');
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
expect(nResults).toEqual(1);
expect(nResults).toEqual(2);
});
it('should delete the weekly ticket 11', async() => {
@ -104,7 +104,7 @@ describe('Ticket descriptor path', () => {
await page.doSearch();
const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult);
expect(nResults).toEqual(6);
expect(nResults).toEqual(7);
});
it('should update the agency then remove it afterwards', async() => {

View File

@ -29,7 +29,7 @@ describe('Ticket expeditions', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
expect(result).toEqual(2);
});
it(`should move one expedition to new ticket with route`, async() => {
@ -45,6 +45,6 @@ describe('Ticket expeditions', () => {
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
expect(result).toEqual(2);
});
});

View File

@ -87,7 +87,7 @@ describe('Ticket Future path', () => {
await page.clearInput(selectors.ticketFuture.futureState);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 5);
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);

View File

@ -49,7 +49,11 @@ describe('Claim summary path', () => {
});
it(`should click on the first sale ID making the item descriptor visible`, async() => {
await page.waitToClick(selectors.claimSummary.firstSaleItemId);
const firstItem = selectors.claimSummary.firstSaleItemId;
await page.evaluate(selectors => {
document.querySelector(selectors).scrollIntoView();
}, firstItem);
await page.click(firstItem);
await page.waitImgLoad(selectors.claimSummary.firstSaleDescriptorImage);
const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover);

View File

@ -66,97 +66,4 @@ describe('Entry import, create and edit buys path', () => {
await page.waitToClick(selectors.globalItems.acceptButton);
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 1);
});
it('should add a new buy', async() => {
await page.waitToClick(selectors.entryBuys.addBuyButton);
await page.write(selectors.entryBuys.secondBuyPackingPrice, '999');
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '999');
await page.write(selectors.entryBuys.secondBuyPrice, '999');
await page.write(selectors.entryBuys.secondBuyGrouping, '999');
await page.write(selectors.entryBuys.secondBuyPacking, '999');
await page.write(selectors.entryBuys.secondBuyWeight, '999');
await page.write(selectors.entryBuys.secondBuyStickers, '999');
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '1');
await page.write(selectors.entryBuys.secondBuyQuantity, '999');
await page.autocompleteSearch(selectors.entryBuys.secondBuyItem, '1');
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 2);
});
it('should edit the newest buy and check data', async() => {
await page.clearInput(selectors.entryBuys.secondBuyPackingPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPackingPrice, '100');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPrice);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPrice, '300');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGrouping);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGrouping, '400');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPacking);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPacking, '500');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyWeight);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyWeight, '600');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyStickers);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyStickers, '700');
await page.keyboard.press('Enter');
await page.waitForSnackbar();
await page.autocompleteSearch(selectors.entryBuys.secondBuyPackage, '94');
await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyQuantity);
await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyQuantity, '800');
await page.keyboard.press('Enter');
await page.reloadSection('entry.card.buy.index');
const secondBuyPackingPrice = await page.getValue(selectors.entryBuys.secondBuyPackingPrice);
const secondBuyGroupingPrice = await page.getValue(selectors.entryBuys.secondBuyGroupingPrice);
const secondBuyPrice = await page.getValue(selectors.entryBuys.secondBuyPrice);
const secondBuyGrouping = await page.getValue(selectors.entryBuys.secondBuyGrouping);
const secondBuyPacking = await page.getValue(selectors.entryBuys.secondBuyPacking);
const secondBuyWeight = await page.getValue(selectors.entryBuys.secondBuyWeight);
const secondBuyStickers = await page.getValue(selectors.entryBuys.secondBuyStickers);
const secondBuyPackage = await page.getValue(selectors.entryBuys.secondBuyPackage);
const secondBuyQuantity = await page.getValue(selectors.entryBuys.secondBuyQuantity);
expect(secondBuyPackingPrice).toEqual('100');
expect(secondBuyGroupingPrice).toEqual('200');
expect(secondBuyPrice).toEqual('300');
expect(secondBuyGrouping).toEqual('400');
expect(secondBuyPacking).toEqual('500');
expect(secondBuyWeight).toEqual('600');
expect(secondBuyStickers).toEqual('700');
expect(secondBuyPackage).toEqual('94');
expect(secondBuyQuantity).toEqual('800');
});
});

View File

@ -8,7 +8,7 @@ describe('Account create and basic data path', () => {
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('developer', 'account');
await page.loginAndModule('itManagement', 'account');
});
afterAll(async() => {

View File

@ -4,8 +4,8 @@ vn-avatar {
display: block;
border-radius: 50%;
overflow: hidden;
height: 36px;
width: 36px;
height: 38px;
width: 38px;
font-size: 22px;
background-color: $color-main;
position: relative;

View File

@ -40,6 +40,8 @@ export default class SmartTable extends Component {
this._options = options;
if (!options) return;
options.defaultSearch = true;
if (options.defaultSearch)
this.displaySearch();

View File

@ -24,7 +24,7 @@ export default class Auth {
initialize() {
let criteria = {
to: state => {
const outLayout = ['login', 'recover-password', 'reset-password'];
const outLayout = ['login', 'recover-password', 'reset-password', 'change-password'];
return !outLayout.some(ol => ol == state.name);
}
};
@ -59,12 +59,12 @@ export default class Auth {
password: password || undefined
};
return this.$http.post('VnUsers/signIn', params).then(
json => this.onLoginOk(json, remember));
return this.$http.post('VnUsers/signIn', params)
.then(json => this.onLoginOk(json, remember));
}
onLoginOk(json, remember) {
this.vnToken.set(json.data.token, remember);
this.vnToken.set(json.data.token, json.data.created, remember);
return this.loadAcls().then(() => {
let continueHash = this.$state.params.continue;

View File

@ -11,3 +11,4 @@ import './report';
import './email';
import './file';
import './date';

View File

@ -9,25 +9,33 @@ export default class Token {
constructor() {
try {
this.token = sessionStorage.getItem('vnToken');
if (!this.token)
this.created = sessionStorage.getItem('vnTokenCreated');
if (!this.token) {
this.token = localStorage.getItem('vnToken');
this.created = localStorage.getItem('vnTokenCreated');
}
} catch (e) {}
}
set(value, remember) {
set(token, created, remember) {
this.unset();
try {
if (remember)
localStorage.setItem('vnToken', value);
else
sessionStorage.setItem('vnToken', value);
if (remember) {
localStorage.setItem('vnToken', token);
localStorage.setItem('vnTokenCreated', created);
} else {
sessionStorage.setItem('vnToken', token);
sessionStorage.setItem('vnTokenCreated', created);
}
} catch (e) {}
this.token = value;
this.token = token;
this.created = created;
}
unset() {
localStorage.removeItem('vnToken');
sessionStorage.removeItem('vnToken');
this.token = null;
this.created = null;
}
}

View File

@ -1,4 +1,11 @@
@import "./variables";
@import "./effects";
@mixin mobile {
@media screen and (max-width: $mobile-width) {
@content;
}
}
@mixin browser($browser) {
html[data-browser*="#{$browser}"] & {
@content;

View File

@ -1,5 +1,3 @@
@import "./util";
$font-size: 11pt;
$menu-width: 256px;
$topbar-height: 56px;

View File

@ -1,4 +1,4 @@
@import "variables";
@import "util";
@keyframes fadein {
from {
@ -16,7 +16,7 @@ vn-background {
background-color: black;
z-index: 14;
@media screen and (max-width: $mobile-width) {
@include mobile {
&.shown {
display: block;
opacity: .3;

View File

@ -0,0 +1,29 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Change password</h5>
<vn-textfield
label="Old password"
ng-model="$ctrl.oldPassword"
vn-name="oldPassword"
type="password"
vn-focus>
</vn-textfield>
<vn-textfield
label="New password"
ng-model="$ctrl.newPassword"
vn-name="newPassword"
type="password"
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
autocomplete="false">
</vn-textfield>
<vn-textfield
label="Repeat password"
ng-model="$ctrl.repeatPassword"
vn-name="repeatPassword"
type="password"
autocomplete="false">
</vn-textfield>
<div class="footer">
<vn-submit label="Change password" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>

View File

@ -0,0 +1,63 @@
import ngModule from '../../module';
const UserError = require('vn-loopback/util/user-error');
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state,
$location
});
}
$onInit() {
if (!this.$state.params.id)
this.$state.go('login');
this.$http.get('UserPasswords/findOne')
.then(res => {
this.passRequirements = res.data;
});
}
submit() {
const userId = this.$state.params.userId;
const newPassword = this.newPassword;
const oldPassword = this.oldPassword;
if (!newPassword)
throw new UserError(`You must enter a new password`);
if (newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`);
const headers = {
Authorization: this.$state.params.id
};
this.$http.post('VnUsers/change-password',
{
id: userId,
oldPassword,
newPassword
},
{headers}
).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Password updated!'));
this.$state.go('login');
});
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
ngModule.vnComponent('vnChangePassword', {
template: require('./index.html'),
controller: Controller,
bindings: {
id: '<'
}
});

View File

@ -0,0 +1,4 @@
Password requirements: >
The password must have at least {{ length }} length characters,
{{nAlpha}} alphabetic characters, {{nUpper}} capital letters, {{nDigits}}
digits and {{nPunct}} symbols (Ex: $%&.)

View File

@ -0,0 +1,9 @@
Change password: Cambiar contraseña
Old password: Antigua contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseña
Password updated!: ¡Contraseña actualizada!
Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud,
{{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}}
dígitos y {{nPunct}} símbolos (Ej: $%&.)

View File

@ -9,6 +9,7 @@ import './login';
import './outLayout';
import './recover-password';
import './reset-password';
import './change-password';
import './module-card';
import './module-main';
import './side-menu/side-menu';

View File

@ -3,13 +3,14 @@ import Component from 'core/lib/component';
import './style.scss';
export class Layout extends Component {
constructor($element, $, vnModules) {
constructor($element, $, vnModules, vnToken) {
super($element, $);
this.modules = vnModules.get();
}
$onInit() {
this.getUserData();
this.getAccessTokenConfig();
}
getUserData() {
@ -30,8 +31,42 @@ export class Layout extends Component {
refresh() {
window.location.reload();
}
getAccessTokenConfig() {
this.$http.get('AccessTokenConfigs').then(json => {
const firtsResult = json.data[0];
if (!firtsResult) return;
this.renewPeriod = firtsResult.renewPeriod;
this.renewInterval = firtsResult.renewInterval;
const intervalMilliseconds = firtsResult.renewInterval * 1000;
this.inservalId = setInterval(this.checkTokenValidity.bind(this), intervalMilliseconds);
});
}
Layout.$inject = ['$element', '$scope', 'vnModules'];
checkTokenValidity() {
const now = new Date();
const differenceMilliseconds = now - new Date(this.vnToken.created);
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
if (differenceSeconds > this.renewPeriod) {
this.$http.post('VnUsers/renewToken')
.then(json => {
if (json.data.token) {
let remember = true;
if (window.sessionStorage.vnToken) remember = false;
this.vnToken.set(json.data.token, json.data.created, remember);
}
});
}
}
$onDestroy() {
clearInterval(this.inservalId);
}
}
Layout.$inject = ['$element', '$scope', 'vnModules', 'vnToken'];
ngModule.vnComponent('vnLayout', {
template: require('./index.html'),

View File

@ -37,4 +37,49 @@ describe('Component vnLayout', () => {
expect(url).not.toBeDefined();
});
});
describe('getAccessTokenConfig()', () => {
it(`should set the renewPeriod and renewInterval properties in localStorage`, () => {
const response = [{
renewPeriod: 100,
renewInterval: 5
}];
$httpBackend.expect('GET', `AccessTokenConfigs`).respond(response);
controller.getAccessTokenConfig();
$httpBackend.flush();
expect(controller.renewPeriod).toBe(100);
expect(controller.renewInterval).toBe(5);
expect(controller.inservalId).toBeDefined();
});
});
describe('checkTokenValidity()', () => {
it(`should not call renewToken and not set vnToken in the controller`, () => {
controller.renewPeriod = 100;
controller.vnToken.created = new Date();
controller.checkTokenValidity();
expect(controller.vnToken.token).toBeNull();
});
it(`should call renewToken and set vnToken properties in the controller`, () => {
const response = {
token: 999,
created: new Date()
};
controller.renewPeriod = 100;
const oneHourBefore = new Date(Date.now() - (60 * 60 * 1000));
controller.vnToken.created = oneHourBefore;
$httpBackend.expect('POST', `VnUsers/renewToken`).respond(response);
controller.checkTokenValidity();
$httpBackend.flush();
expect(controller.vnToken.token).toBe(999);
expect(controller.vnToken.created).toEqual(response.created);
});
});
});

View File

@ -1,4 +1,4 @@
@import "effects";
@import "util";
vn-layout {
& > vn-topbar {
@ -134,7 +134,7 @@ vn-layout {
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
& > vn-topbar {
& > .start > .logo {
display: none;

View File

@ -2,8 +2,6 @@
vn-id="model"
url="{{$ctrl.url}}"
filter="$ctrl.filter"
link="{originFk: $ctrl.originId}"
where="{changedModel: $ctrl.changedModel, changedModelId: $ctrl.changedModelId}"
data="$ctrl.logs"
order="creationDate DESC, id DESC"
limit="20">
@ -16,51 +14,69 @@
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-sm vn-px-sm">
<div class="change vn-mb-sm" ng-repeat="log in $ctrl.logs">
<div class="left">
<vn-avatar class="vn-mt-xs"
ng-class="::{system: !log.user}"
val="{{::log.user ? log.user.nickname : $ctrl.$t('System')}}"
ng-click="$ctrl.showWorkerDescriptor($event, log)">
<img
ng-if="::log.user.image"
ng-src="/api/Images/user/160x160/{{::log.userFk}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
<div class="arrow bg-panel"></div>
class="vn-w-sm vn-px-sm vn-pb-xl">
<div class="origin-log" ng-repeat="originLog in $ctrl.logTree">
<div class="origin-info vn-mb-md" ng-if="::$ctrl.logTree.length > 1">
<h6 class="origin-id">
{{::$ctrl.modelI18n}} #{{::originLog.originFk}}
</h6>
<div class="line"></div>
</div>
<vn-card class="detail">
<div class="header vn-pa-sm">
<div
class="action-date text-secondary text-caption vn-mr-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
<div class="user-log vn-mb-sm" ng-repeat="userLog in ::originLog.logs">
<div class="timeline">
<vn-avatar
ng-class="::{system: !userLog.user}"
val="{{::userLog.user ? userLog.user.nickname : $ctrl.$t('System')}}"
ng-click="$ctrl.showWorkerDescriptor($event, userLog)">
<img
ng-if="::userLog.user.image"
ng-src="/api/Images/user/160x160/{{::userLog.userFk}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
<div class="arrow bg-panel" ng-if="::$ctrl.byRecord"></div>
<div class="line"></div>
</div>
<div class="user-changes">
<div class="model-log" ng-repeat="modelLog in ::userLog.logs">
<div class="model-info vn-my-sm" ng-if="::!$ctrl.byRecord">
<vn-icon
class="action vn-mr-xs"
icon="filter_alt"
translate-attr="{title: 'Show all record changes'}"
ng-click="$ctrl.filterByRecord(modelLog)">
</vn-icon>
<span class="model-name"
ng-if="::$ctrl.showModelName && modelLog.model"
ng-style="::{backgroundColor: $ctrl.hashToColor(modelLog.model)}"
title="{{::modelLog.model}}">
{{::modelLog.modelI18n}}
</span>
<span class="model-id" ng-if="::modelLog.id">#{{::modelLog.id}}</span>
<span class="model-value" title="{{::modelLog.showValue}}">{{::modelLog.showValue}}</span>
</div>
<vn-card class="changes-log vn-mb-xs" ng-repeat="log in ::modelLog.logs">
<div class="change-info vn-pa-sm">
<div
class="date text-secondary text-caption vn-mr-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<div>
<vn-icon
class="pit vn-ml-xs"
icon="preview"
translate-attr="::{title: 'View record at this point in time'}"
ng-show="::log.action != 'insert'"
ng-click="$ctrl.viewPitInstance($event, log.id, modelLog)">
</vn-icon>
<vn-icon
class="action vn-ml-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<div class="action-model">
<span class="model-name"
ng-if="::$ctrl.showModelName && log.changedModel"
ng-style="::{backgroundColor: $ctrl.hashToColor(log.changedModel)}"
title="{{::log.changedModel}}">
{{::log.changedModelI18n}}
</span>
</div>
</div>
<div
class="model vn-pb-sm vn-px-sm"
title="{{::log.changedModelValue}}"
ng-if="::log.changedModelId || log.changedModelValue">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<span class="model-value">{{::log.changedModelValue}}</span>
</div>
<div class="changes vn-pa-sm"
<div class="change-detail vn-pa-sm"
ng-class="{expanded: log.expand}"
ng-if="::log.props.length || log.description">
<vn-icon
@ -75,16 +91,16 @@
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span>
<vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value>
<vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'">
<vn-json-value value="::prop.old"></vn-json-value>
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
</div>
@ -95,11 +111,40 @@
</vn-card>
</div>
</div>
</div>
</div>
</div>
</vn-data-viewer>
<vn-float-button
ng-if="$ctrl.hasFilter"
icon="filter_alt_off"
translate-attr="{title: 'Quit filter'}"
ng-click="$ctrl.resetFilter()"
fixed-bottom-right>
</vn-float-button>
<vn-side-menu side="right">
<form vn-vertical
ng-model-options="{updateOn: 'change blur'}"
class="vn-pa-md filter">
<vn-textfield
label="Search"
ng-model="filter.search">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by id or concept"
pointer>
</vn-icon>
</append>
</vn-textfield>
<vn-autocomplete
label="Entity"
ng-model="filter.changedModel"
value-field="changedModel"
show-field="changedModelI18n"
data="$ctrl.models"
class="changed-model">
</vn-autocomplete>
<vn-vertical>
<vn-radio
label="All"
@ -145,30 +190,16 @@
</tpl-item>
</vn-autocomplete>
<vn-textfield
label="Search"
ng-model="filter.search">
label="Changes"
ng-model="filter.changes">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by id or concept"
vn-tooltip="Search by changes"
pointer>
</vn-icon>
</append>
</vn-textfield>
<vn-autocomplete
label="Entity"
ng-model="filter.changedModel"
value-field="changedModel"
show-field="changedModelI18n"
data="$ctrl.models"
class="changed-model">
</vn-autocomplete>
<!-- FIXME: Cannot use LIKE with JSON columns
<vn-textfield
label="Changes"
ng-model="filter.changes">
</vn-textfield>
-->
<vn-vertical>
<vn-check
label="Creates"
@ -195,19 +226,35 @@
label="To"
ng-model="filter.to">
</vn-date-picker>
<vn-button-bar vn-vertical>
<vn-button
label="Filter"
ng-click="$ctrl.applyFilter(filter)">
</vn-button>
<vn-button
label="Reset"
class="flat"
ng-click="$ctrl.resetFilter()"
ng-if="model.userFilter">
</vn-button>
</vn-button-bar>
</form>
</vn-side-menu>
<vn-worker-descriptor-popover vn-id="workerDescriptor">
<vn-popover vn-id="instance-popover">
<tpl-body class="vn-log-instance">
<vn-spinner
ng-if="$ctrl.instance.canceler"
class="loading vn-pa-sm"
enable="true">
</vn-spinner>
<div
ng-if="!$ctrl.instance.canceler" class="instance">
<h6 class="header vn-pa-sm">
{{$ctrl.instance.modelLog.modelI18n}} #{{$ctrl.instance.modelLog.id}}
</h6>
<div class="change-detail vn-pa-sm">
<div ng-if="$ctrl.instance.props"
ng-repeat="prop in $ctrl.instance.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-log-value val="::prop.val"></vn-log-value>
</div>
<div ng-if="!$ctrl.instance.props" translate>
No data
</div>
</div>
</div>
</tpl-body>
</vn-popover>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>

View File

@ -3,7 +3,10 @@ import Section from '../section';
import {hashToColor} from 'core/lib/string';
import './style.scss';
const validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/;
const validDate = new RegExp(
/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])/.source
+ /T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/.source
);
export default class Controller extends Section {
constructor($element, $) {
@ -28,6 +31,20 @@ export default class Controller extends Section {
select: 'visibility'
};
this.filter = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description'
],
include: [{
relation: 'user',
scope: {
@ -48,6 +65,11 @@ export default class Controller extends Section {
this.today.setHours(0, 0, 0, 0);
}
$onInit() {
const match = this.url?.match(/(.*)Logs$/);
this.modelI18n = match && this.translateModel(match[1]);
}
$postLink() {
this.resetFilter();
this.$.$watch(
@ -63,29 +85,75 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
this.logTree = [];
if (!value) return;
const empty = {};
const validations = window.validations;
for (const log of value) {
const oldValues = log.oldInstance || empty;
const newValues = log.newInstance || empty;
let originLog;
let userLog;
let modelLog;
let nLogs;
for (let i = 0; i < value.length; i++) {
const log = value[i];
const prevLog = i > 0 ? value[i - 1] : null;
const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = firstUpper(locale.name) || log.changedModel;
let props = Object.keys(oldValues).concat(Object.keys(newValues));
props = [...new Set(props)];
// Origin
log.props = [];
for (const prop of props) {
log.props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: this.castJsonValue(oldValues[prop]),
new: this.castJsonValue(newValues[prop])
const originChanged = !prevLog
|| log.originFk != prevLog.originFk;
if (originChanged) {
this.logTree.push(originLog = {
originFk: log.originFk,
logs: []
});
}
log.props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
// User
const userChanged = originChanged
|| log.userFk != prevLog.userFk
|| nLogs >= 5;
if (userChanged) {
originLog.logs.push(userLog = {
user: log.user,
userFk: log.userFk,
logs: []
});
nLogs = 0;
}
nLogs++;
// Model
const modelChanged = userChanged
|| log.changedModel != prevLog.changedModel
|| log.changedModelId != prevLog.changedModelId;
if (modelChanged) {
userLog.logs.push(modelLog = {
model: log.changedModel,
modelI18n: firstUpper(locale.name) || log.changedModel,
id: log.changedModelId,
showValue: log.changedModelValue,
logs: []
});
}
modelLog.logs.push(log);
// Changes
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
let propNames = Object.keys(olds).concat(Object.keys(vals));
propNames = [...new Set(propNames)];
log.props = this.parseProps(propNames, locale, vals, olds);
}
}
@ -96,27 +164,82 @@ export default class Controller extends Section {
set models(value) {
this._models = value;
if (!value) return;
for (const model of value) {
const name = model.changedModel;
model.changedModelI18n =
firstUpper(window.validations[name]?.locale?.name) || name;
}
for (const model of value)
model.changedModelI18n = this.translateModel(model.changedModel);
}
get showModelName() {
return !(this.changedModel && this.changedModelId);
}
parseProps(propNames, locale, vals, olds) {
const castJsonValue = this.castJsonValue;
const props = [];
for (const prop of propNames) {
if (prop.endsWith('$')) continue;
props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
val: getVal(vals, prop),
old: olds && getVal(olds, prop)
});
}
props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
function getVal(vals, prop) {
let val; let id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
}
return props;
}
viewPitInstance(event, id, modelLog) {
if (this.instance?.canceler)
this.instance.canceler.resolve();
const canceler = this.$q.defer();
this.instance = {
modelLog,
canceler
};
const options = {timeout: canceler.promise};
this.$http.get(`${this.url}/${id}/pitInstance`, options)
.then(res => {
const instance = res.data;
const propNames = Object.keys(instance);
const locale = window.validations[modelLog.model]?.locale || {};
this.instance.props = this.parseProps(propNames, locale, instance);
})
.finally(() => {
this.instance.canceler = null;
this.$.$applyAsync(() => this.$.instancePopover.relocate());
});
this.$.instancePopover.show(event);
}
translateModel(name) {
return firstUpper(window.validations[name]?.locale?.name) || name;
}
castJsonValue(value) {
return typeof value === 'string' && validDate.test(value)
? new Date(value)
: value;
}
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
relativeDate(dateVal) {
if (dateVal == null) return '';
const date = new Date(dateVal);
@ -146,80 +269,102 @@ export default class Controller extends Section {
applyFilter() {
const filter = this.$.filter;
function getParam(prop, value) {
const getParam = (prop, value) => {
if (value == null || value == '') return null;
switch (prop) {
case 'search':
if (/^[0-9]+$/.test(value))
return {changedModelId: value};
if (/^\s*[0-9]+\s*$/.test(value) || this.byRecord)
return {changedModelId: value.trim()};
else
return {changedModelValue: {like: `%${value}%`}};
case 'changes':
return {or: [
{oldInstance: {like: `%${value}%`}},
{newInstance: {like: `%${value}%`}},
{oldJson: {like: `%${value}%`}},
{newJson: {like: `%${value}%`}},
{description: {like: `%${value}%`}}
]};
case 'who':
switch (value) {
case 'all':
return null;
case 'user':
return {userFk: {neq: null}};
case 'system':
return {userFk: null};
case 'all':
default:
return null;
}
case 'actions':
case 'actions': {
const inq = [];
for (const action in value) {
if (value[action])
inq.push(action);
}
return inq.length ? {action: {inq}} : null;
}
case 'from':
if (filter.to) {
if (filter.to)
return {creationDate: {gte: value}};
} else {
else {
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {between: [value, to]}};
}
case 'to':
case 'to': {
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {lte: to}};
}
case 'userFk':
return filter.who != 'system'
? {[prop]: value} : null;
default:
return {[prop]: value};
}
}
};
this.hasFilter = false;
const and = [];
if (!filter.search || !filter.changedModel)
this.byRecord = false;
if (!this.byRecord)
and.push({originFk: this.originId});
for (const prop in filter) {
const param = getParam(prop, filter[prop]);
if (param) and.push(param);
if (param) {
and.push(param);
this.hasFilter = true;
}
}
const lbFilter = and.length ? {where: {and}} : null;
return this.$.model.applyFilter(lbFilter);
}
filterByRecord(modelLog) {
this.byRecord = true;
this.$.filter = {
who: 'all',
search: modelLog.id,
changedModel: modelLog.model
};
}
searchUser(search) {
if (/^[0-9]+$/.test(search)) {
if (/^[0-9]+$/.test(search))
return {id: search};
} else {
else {
return {or: [
{name: search},
{nickname: {like: `%${search}%`}}
]}
]};
}
}
showWorkerDescriptor(event, log) {
if (log.user?.worker)
this.$.workerDescriptor.show(event.target, log.userFk);
showWorkerDescriptor(event, userLog) {
if (userLog.user?.worker)
this.$.workerDescriptor.show(event.target, userLog.userFk);
}
}
@ -238,3 +383,12 @@ ngModule.vnComponent('vnLog', {
url: '@'
}
});
ngModule.component('vnLogValue', {
template:
'<vn-json-value value="::$ctrl.val.val"></vn-json-value>' +
'<span ng-if="::$ctrl.val.id" class="id-value"> #{{::$ctrl.val.id}}</span>',
bindings: {
val: '<?',
}
});

View File

@ -2,6 +2,9 @@ Date: Fecha
Concept: Concepto
Search: Buscar
Search by id or concept: Buscar por identificador o concepto
Search by changes: |
Buscar por cambios. Los atributos deben buscarse por su nombre interno,
para obtenerlo situar el cursor sobre el atributo.
Entity: Entidad
Action: Acción
Author: Autor
@ -13,9 +16,13 @@ Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
All: Todo
System: Sistema
Details: Detalles
note: nota
Changes: Cambios
today: hoy
yesterday: ayer
Show all record changes: Mostrar todos los cambios realizados en el registro
View record at this point in time: Ver el registro en este punto
Quit filter: Quitar filtro

View File

@ -1,14 +1,49 @@
@import "variables";
@import "effects";
@import "util";
vn-log {
.change {
.origin-log {
&:first-child > .origin-info {
margin-top: 0;
}
& > .origin-info {
display: flex;
align-items: center;
margin-top: 28px;
gap: 6px;
& > .origin-id {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: $color-font-secondary;
margin: 0;
}
& > .line {
flex-grow: 1;
background-color: $color-font-secondary;
height: 2px;
}
}
}
.user-log {
display: flex;
& > .left {
& > .timeline {
position: relative;
padding-right: 10px;
width: 38px;
min-width: 38px;
flex-grow: auto;
& > .arrow {
height: 8px;
width: 8px;
position: absolute;
transform: rotateY(0deg) rotate(45deg);
top: 15px;
right: -4px;
z-index: 1;
}
& > vn-avatar {
cursor: pointer;
@ -16,58 +51,97 @@ vn-log {
background-color: $color-main !important;
}
}
& > .arrow {
height: 8px;
width: 8px;
position: absolute;
transform: rotateY(0deg) rotate(45deg);
top: 18px;
right: -4px;
z-index: 1;
}
& > .line {
position: absolute;
background-color: $color-main;
width: 2px;
left: 17px;
left: 18px;
z-index: -1;
top: 44px;
bottom: -8px;
bottom: -2px;
}
}
&:last-child > .left > .line {
&:last-child > .timeline > .line {
display: none;
}
.detail {
position: relative;
& > .user-changes {
flex-grow: 1;
width: 100%;
border-radius: 2px;
overflow: hidden;
& > .header {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .action-model {
display: inline-flex;
}
}
.model-log {
& > .model-info {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-height: 22px;
& > .model-name {
display: inline-block;
padding: 2px 5px;
color: $color-font-dark;
border-radius: 8px;
vertical-align: middle;
}
& > .model-value {
font-style: italic;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
}
& > vn-icon[icon="filter_alt"] {
@extend %clickable-light;
vertical-align: middle;
font-size: 18px;
color: $color-font-secondary;
float: right;
display: none;
@include mobile {
display: initial;
}
}
}
&:hover > .model-info > vn-icon[icon="filter_alt"] {
display: initial;
}
}
.changes-log {
position: relative;
max-width: 100%;
width: 100%;
border-radius: 2px;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
& > .change-info {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
& > .date {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-overflow: ellipsis;
}
}
& > .action-date {
& > div {
white-space: nowrap;
& > vn-icon.pit {
@extend %clickable-light;
vertical-align: middle;
font-size: 20px;
color: $color-font-secondary;
display: none;
@include mobile {
display: inline-block;
}
}
& > .action {
display: inline-flex;
align-items: center;
@ -77,7 +151,7 @@ vn-log {
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 1.4em;
font-size: 18px;
&.notice {
background-color: $color-notice-medium
@ -93,23 +167,11 @@ vn-log {
}
}
}
}
& > .model {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& > .model-value {
font-style: italic;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9rem;
&:hover vn-icon.pit {
display: inline-block;
}
}
}
}
.changes {
& > .change-detail {
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
color: $color-font-light;
@ -144,3 +206,32 @@ vn-log {
}
}
}
.id-value {
font-size: .9rem;
color: $color-font-secondary;
}
}
.vn-log-instance {
display: block;
& > .loading {
display: flex;
justify-content: center;
}
& > .instance {
min-width: 180px;
max-width: 400px;
& > .header {
background-color: $color-main;
color: $color-font-dark;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 0;
}
& > .change-detail {
color: $color-font-light;
}
}
}

View File

@ -5,10 +5,11 @@ import './style.scss';
* A simple login form.
*/
export default class Controller {
constructor($, $element, vnAuth) {
constructor($, $element, $state, vnAuth) {
Object.assign(this, {
$,
$element,
$state,
vnAuth,
user: localStorage.getItem('lastUser'),
remember: true
@ -22,11 +23,15 @@ export default class Controller {
localStorage.setItem('lastUser', this.user);
this.loading = false;
})
.catch(err => {
.catch(req => {
this.loading = false;
this.password = '';
this.focusUser();
throw err;
const err = req.data?.error;
if (err?.code == 'passExpired')
this.$state.go('change-password', err.details.token);
throw req;
});
}
@ -35,7 +40,7 @@ export default class Controller {
this.$.userField.focus();
}
}
Controller.$inject = ['$scope', '$element', 'vnAuth'];
Controller.$inject = ['$scope', '$element', '$state', 'vnAuth'];
ngModule.vnComponent('vnLogin', {
template: require('./index.html'),

View File

@ -64,4 +64,25 @@ vn-out-layout{
a{
color: $color-primary;
}
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}

View File

@ -1,5 +1,4 @@
import ngModule from '../../module';
import './style.scss';
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state) {

View File

@ -1,24 +0,0 @@
@import "variables";
vn-recover-password{
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}

View File

@ -1,5 +1,5 @@
import ngModule from '../../module';
import './style.scss';
const UserError = require('vn-loopback/util/user-error');
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {

View File

@ -1,24 +0,0 @@
@import "variables";
vn-reset-password{
.footer {
margin-top: 32px;
text-align: center;
position: relative;
& > .vn-submit {
display: block;
& > input {
display: block;
width: 100%;
}
}
& > .spinner-wrapper {
position: absolute;
width: 0;
top: 3px;
right: -8px;
overflow: visible;
}
}
}

View File

@ -36,6 +36,12 @@ function config($stateProvider, $urlRouterProvider) {
description: 'Reset password',
template: '<vn-reset-password></vn-reset-password>'
})
.state('change-password', {
parent: 'outLayout',
url: '/change-password?id&userId',
description: 'Change password',
template: '<vn-change-password></vn-change-password>'
})
.state('home', {
parent: 'layout',
url: '/',

View File

@ -1,5 +1,4 @@
@import "./variables";
@import "./effects";
@import "./util";
form vn-horizontal {
align-items: center;
@ -22,7 +21,7 @@ form vn-horizontal {
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
flex-direction: column;
align-items: initial;

View File

@ -0,0 +1,91 @@
const NotFoundError = require('vn-loopback/util/not-found-error');
module.exports = Self => {
Self.remoteMethod('pitInstance', {
description: 'Gets the status of instance at specific point in time',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The log id',
required: true
}
],
returns: {
type: [Self],
root: true
},
http: {
path: `/:id/pitInstance`,
verb: 'GET'
}
});
Self.pitInstance = async function(id) {
const log = await Self.findById(id, {
fields: [
'changedModel',
'changedModelId',
'creationDate'
]
});
if (!log)
throw new NotFoundError();
const where = {
changedModel: log.changedModel,
changedModelId: log.changedModelId
};
// Fetch creation and all update logs for record up to requested log
const createdWhere = {
action: 'insert',
creationDate: {lte: log.creationDate}
};
const createdLog = await Self.findOne({
fields: ['id', 'creationDate', 'newInstance'],
where: Object.assign(createdWhere, where),
order: 'creationDate DESC, id DESC'
});
const instance = {};
let logsWhere = {
action: 'update'
};
if (createdLog) {
Object.assign(instance, createdLog.newInstance);
Object.assign(logsWhere, {
creationDate: {between: [
createdLog.creationDate,
log.creationDate
]},
id: {between: [
Math.min(id, createdLog.id),
Math.max(id, createdLog.id)
]}
});
} else {
Object.assign(logsWhere, {
creationDate: {lte: log.creationDate},
id: {lte: id}
});
}
const logs = await Self.find({
fields: ['newInstance'],
where: Object.assign(logsWhere, where),
order: 'creationDate, id'
});
if (!logs.length && !createdLog)
throw new NotFoundError('No logs found for record');
// Merge all logs in order into one instance
for (const log of logs)
Object.assign(instance, log.newInstance);
return instance;
};
};

View File

@ -5,6 +5,7 @@ module.exports = function(Self) {
Self.super_.setup.call(this);
require('../methods/log/editors')(this);
require('../methods/log/models')(this);
require('../methods/log/pitInstance')(this);
}
});
};

View File

@ -1,4 +1,61 @@
{
"name": "Log",
"base": "VnModel"
"base": "VnModel",
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"oldJson": {
"type": "String",
"mysql": {"columnName": "oldInstance"}
},
"newJson": {
"type": "String",
"mysql": {"columnName": "newInstance"}
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -2,7 +2,6 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const UserError = require('vn-loopback/util/user-error');
const utils = require('loopback/lib/utils');
const {util} = require('webpack');
module.exports = function(Self) {
Self.ParameterizedSQL = ParameterizedSQL;
@ -196,8 +195,45 @@ module.exports = function(Self) {
/*
* Shortcut to VnMySQL.executeP()
*/
rawSql(query, params, options, cb) {
return this.dataSource.connector.executeP(query, params, options, cb);
async rawSql(query, params, options) {
const userId = options?.userId;
const connector = this.dataSource.connector;
let conn;
let res;
try {
if (userId) {
if (!options.transaction) {
options = Object.assign({}, options);
conn = await new Promise((resolve, reject) => {
connector.client.getConnection(function(err, conn) {
if (err)
reject(err);
else
resolve(conn);
});
});
options.transaction = {
connection: conn,
connector
};
}
await connector.executeP(
'CALL account.myUser_loginWithName((SELECT name FROM account.user WHERE id = ?))',
[userId], options
);
}
res = await connector.executeP(query, params, options);
if (userId)
await connector.executeP('CALL account.myUser_logout()', null, options);
} finally {
if (conn) conn.release();
}
return res;
},
/*

View File

@ -115,7 +115,7 @@
"This client is not invoiceable": "This client is not invoiceable",
"INACTIVE_PROVIDER": "Inactive provider",
"reference duplicated": "reference duplicated",
"The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"The PDF document does not exist": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"This item is not available": "This item is not available",
"Deny buy request": "Purchase request for ticket id [{{ticketId}}]({{{url}}}) has been rejected. Reason: {{observation}}",
"The type of business must be filled in basic data": "The type of business must be filled in basic data",
@ -172,6 +172,9 @@
"Comment added to client": "Comment added to client",
"This ticket is already a refund": "This ticket is already a refund",
"A claim with that sale already exists": "A claim with that sale already exists",
"Pass expired": "The password has expired, change it from Salix",
"Can't transfer claimed sales": "Can't transfer claimed sales",
"Invalid quantity": "Invalid quantity"
"Invalid quantity": "Invalid quantity",
"Failed to upload delivery note": "Error to upload delivery note {{id}}",
"Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address"
}

View File

@ -77,7 +77,6 @@
"This ticket can not be modified": "Este ticket no puede ser modificado",
"The introduced hour already exists": "Esta hora ya ha sido introducida",
"INFINITE_LOOP": "Existe una dependencia entre dos Jefes",
"The sales of the current ticket can't be modified": "Las lineas de este ticket no pueden ser modificadas",
"The sales of the receiver ticket can't be modified": "Las lineas del ticket al que envias no pueden ser modificadas",
"NO_AGENCY_AVAILABLE": "No hay una zona de reparto disponible con estos parámetros",
"ERROR_PAST_SHIPMENT": "No puedes seleccionar una fecha de envío en pasado",
@ -212,7 +211,7 @@
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a financialBoss": "No puedes cambiar el cŕedito establecido a cero por un jefe de finanzas",
"Amounts do not match": "Las cantidades no coinciden",
"The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The PDF document does not exist": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos",
"You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días",
"The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día",
@ -259,14 +258,14 @@
"App name does not exist": "El nombre de aplicación no es válido",
"Try again": "Vuelve a intentarlo",
"Aplicación bloqueada por el usuario 9": "Aplicación bloqueada por el usuario 9",
"Failed to upload file": "Error al subir archivo",
"Failed to upload delivery note": "Error al subir albarán {{id}}",
"The DOCUWARE PDF document does not exists": "El documento PDF Docuware no existe",
"It is not possible to modify tracked sales": "No es posible modificar líneas de pedido que se hayan empezado a preparar",
"It is not possible to modify sales that their articles are from Floramondo": "No es posible modificar líneas de pedido cuyos artículos sean de Floramondo",
"It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas",
"A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.",
"There is no assigned email for this client": "No hay correo asignado para este cliente",
"Exists an invoice with a previous date": "Existe una factura con fecha anterior",
"Exists an invoice with a future date": "Existe una factura con fecha posterior",
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite",
"Warehouse inventory not set": "El almacén inventario no está establecido",
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
@ -291,7 +290,13 @@
"isTaxDataChecked": "Datos comprobados",
"comercialId": "Id comercial",
"comercialName": "Comercial",
"Pass expired": "La contraseña ha caducado, cambiela desde Salix",
"Invalid NIF for VIES": "Invalid NIF for VIES",
"Ticket does not exist": "Este ticket no existe",
"Ticket is already signed": "Este ticket ya ha sido firmado"
"Ticket is already signed": "Este ticket ya ha sido firmado",
"Fecha fuera de rango": "Fecha fuera de rango",
"Error while generating PDF": "Error al generar PDF",
"Error when sending mail to client": "Error al enviar el correo al cliente",
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado"
}

View File

@ -0,0 +1,3 @@
name: account
columns:
id: id

View File

@ -0,0 +1,3 @@
name: cuenta
columns:
id: id

View File

@ -0,0 +1,5 @@
name: mail alias
columns:
id: id
mailAlias: alias
account: account

View File

@ -0,0 +1,5 @@
name: alias de correo
columns:
id: id
mailAlias: alias
account: cuenta

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