Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4074-download-user-ACL
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alexandre Riera 2023-06-16 07:40:51 +02:00
commit d8b9b99875
99 changed files with 1331 additions and 723 deletions

View File

@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
-
## [2324.01] - 2023-06-08
## [2324.01] - 2023-06-15
### Added
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
@ -41,7 +41,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

@ -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

@ -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,34 +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'
},
{
arg: 'dialog',
type: 'string',
description: 'The dialog'
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';
@ -38,104 +38,114 @@ module.exports = Self => {
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
// get delivery note
const deliveryNote = await models.Ticket.deliveryNotePdf(ctx, {
id,
type: 'deliveryNote'
});
// get ticket data
const ticket = await models.Ticket.findById(id, {
include: [{
relation: 'client',
scope: {
fields: ['id', 'socialName', 'fi']
}
}]
});
// upload file
const templateJson = {
'Fields': [
{
'FieldName': 'N__ALBAR_N',
'ItemElementName': 'string',
'Item': id,
},
{
'FieldName': 'CIF_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().fi,
},
{
'FieldName': 'CODIGO_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().id,
},
{
'FieldName': 'NOMBRE_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().socialName,
},
{
'FieldName': 'FECHA_FACTURA',
'ItemElementName': 'date',
'Item': ticket.shipped,
},
{
'FieldName': 'TOTAL_FACTURA',
'ItemElementName': 'Decimal',
'Item': ticket.totalWithVat,
},
{
'FieldName': 'ESTADO',
'ItemElementName': 'string',
'Item': 'Pendiente procesar',
},
{
'FieldName': 'FIRMA_',
'ItemElementName': 'string',
'Item': 'Si',
},
{
'FieldName': 'FILTRO_TABLET',
'ItemElementName': 'string',
'Item': 'Tablet1',
}
]
};
if (process.env.NODE_ENV != 'production')
throw new UserError('Action not allowed on the test environment');
// delete old
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, false);
if (docuwareFile) {
const deleteJson = {
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
};
const deleteUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`;
await axios.put(deleteUri, deleteJson, options.headers);
}
const uploadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`;
const FormData = require('form-data');
const data = new FormData();
data.append('document', JSON.stringify(templateJson), 'schema.json');
data.append('file[]', deliveryNote[0], 'file.pdf');
const uploadOptions = {
headers: {
'Content-Type': 'multipart/form-data',
'X-File-ModifiedDate': Date.vnNew(),
'Cookie': options.headers.headers.Cookie,
...data.getHeaders()
},
};
return await axios.post(uploadUri, data, uploadOptions)
.catch(() => {
throw new UserError('Failed to upload file');
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', 'name', 'fi']
}
}]
});
// upload file
const templateJson = {
'Fields': [
{
'FieldName': 'N__ALBAR_N',
'ItemElementName': 'string',
'Item': id,
},
{
'FieldName': 'CIF_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().fi,
},
{
'FieldName': 'CODIGO_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().id,
},
{
'FieldName': 'NOMBRE_PROVEEDOR',
'ItemElementName': 'string',
'Item': ticket.client().name + ' - ' + id,
},
{
'FieldName': 'FECHA_FACTURA',
'ItemElementName': 'date',
'Item': ticket.shipped,
},
{
'FieldName': 'TOTAL_FACTURA',
'ItemElementName': 'Decimal',
'Item': ticket.totalWithVat,
},
{
'FieldName': 'ESTADO',
'ItemElementName': 'string',
'Item': 'Pendiente procesar',
},
{
'FieldName': 'FIRMA_',
'ItemElementName': 'string',
'Item': 'Si',
},
{
'FieldName': 'FILTRO_TABLET',
'ItemElementName': 'string',
'Item': 'Tablet1',
}
]
};
if (process.env.NODE_ENV != 'production')
throw new UserError('Action not allowed on the test environment');
// delete old
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, false);
if (docuwareFile) {
const deleteJson = {
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
};
const deleteUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`;
await axios.put(deleteUri, deleteJson, options.headers);
}
const uploadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`;
const FormData = require('form-data');
const data = new FormData();
data.append('document', JSON.stringify(templateJson), 'schema.json');
data.append('file[]', deliveryNote[0], 'file.pdf');
const uploadOptions = {
headers: {
'Content-Type': 'multipart/form-data',
'X-File-ModifiedDate': Date.vnNew(),
'Cookie': options.headers.headers.Cookie,
...data.getHeaders()
},
};
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

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

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) {

View File

@ -108,4 +108,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,7 +56,10 @@
},
"hasGrant": {
"type": "boolean"
}
},
"passExpired": {
"type": "date"
}
},
"relations": {
"role": {

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

@ -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,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

@ -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
@ -2732,7 +2736,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
@ -2897,3 +2902,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
CREATE OR REPLACE 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
CREATE OR REPLACE 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`.`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"]',
@ -523,11 +523,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 +572,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',
@ -712,7 +712,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"]',
@ -736,7 +736,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"]',
@ -810,7 +810,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: {
@ -1216,7 +1216,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

@ -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

@ -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

@ -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

@ -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,8 +59,8 @@ 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) {

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

@ -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

@ -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;

View File

@ -172,6 +172,8 @@
"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}}"
}

View File

@ -77,14 +77,13 @@
"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",
"The current ticket can't be modified": "El ticket actual no puede ser modificado",
"The current claim can't be modified": "La reclamación actual no puede ser modificada",
"The sales of this ticket can't be modified": "Las lineas de este ticket no pueden ser modificadas",
"The sales do not exists": "La(s) línea(s) seleccionada(s) no existe(n)",
"The sales do not exists": "La(s) línea(s) seleccionada(s) no existe(n)",
"Please select at least one sale": "Por favor selecciona al menos una linea",
"All sales must belong to the same ticket": "Todas las lineas deben pertenecer al mismo ticket",
"NO_ZONE_FOR_THIS_PARAMETERS": "Para este día no hay ninguna zona configurada",
@ -259,7 +258,7 @@
"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",
@ -291,6 +290,7 @@
"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"

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

View File

@ -1,6 +1,6 @@
module.exports = Self => {
Self.remoteMethodCtx('changePassword', {
Self.remoteMethod('changePassword', {
description: 'Changes the user password',
accessType: 'WRITE',
accepts: [
@ -27,9 +27,7 @@ module.exports = Self => {
}
});
Self.changePassword = async function(ctx, id, oldPassword, newPassword) {
await Self.rawSql(`CALL account.user_changePassword(?, ?, ?)`,
[id, oldPassword, newPassword], {userId: ctx.req.accessToken.userId});
await Self.app.models.Account.syncById(id, newPassword);
Self.changePassword = async function(id, oldPassword, newPassword) {
await Self.app.models.VnUser.changePassword(id, oldPassword, newPassword);
};
};

View File

@ -1,5 +1,5 @@
module.exports = Self => {
Self.remoteMethodCtx('setPassword', {
Self.remoteMethod('setPassword', {
description: 'Sets the user password',
accessType: 'WRITE',
accepts: [
@ -21,9 +21,7 @@ module.exports = Self => {
}
});
Self.setPassword = async function(ctx, id, newPassword) {
await Self.rawSql(`CALL account.user_setPassword(?, ?)`,
[id, newPassword], {userId: ctx.req.accessToken.userId});
await Self.app.models.Account.syncById(id, newPassword);
Self.setPassword = async function(id, newPassword) {
await Self.app.models.VnUser.setPassword(id, newPassword);
};
};

View File

@ -2,12 +2,21 @@ const {models} = require('vn-loopback/server/server');
describe('account changePassword()', () => {
it('should throw an error when old password is wrong', async() => {
const ctx = {req: {accessToken: {userId: 9}}};
let err;
await models.Account.changePassword(ctx, 1, 'wrongPassword', 'nightmare.9999')
.catch(error => err = error.sqlMessage);
let error;
try {
await models.Account.changePassword(1, 'wrongPassword', 'nightmare.9999');
} catch (e) {
error = e.message;
}
expect(err).toBeDefined();
expect(err).toEqual('Invalid password');
expect(error).toContain('Invalid current password');
});
it('should change password', async() => {
try {
await models.Account.changePassword(70, 'nightmare', 'nightmare.9999');
} catch (e) {
expect(e).toBeUndefined();
}
});
});

View File

@ -1,15 +1,14 @@
const {models} = require('vn-loopback/server/server');
describe('Account setPassword()', () => {
const ctx = {req: {accessToken: {userId: 9}}};
it('should throw an error when password does not meet requirements', async() => {
let req = models.Account.setPassword(ctx, 1, 'insecurePass');
let req = models.Account.setPassword(1, 'insecurePass');
await expectAsync(req).toBeRejected();
});
it('should update password when it passes requirements', async() => {
let req = models.Account.setPassword(ctx, 1, 'Very$ecurePa22.');
let req = models.Account.setPassword(1, 'Very$ecurePa22.');
await expectAsync(req).toBeResolved();
});

View File

@ -24,8 +24,8 @@ module.exports = Self => {
}
});
Self.syncById = async function(id, password, force) {
let user = await Self.app.models.VnUser.findById(id, {fields: ['name']});
await Self.sync(user.name, password, force);
Self.syncById = async function(id, password, force, options) {
let user = await Self.app.models.VnUser.findById(id, {fields: ['name']}, options);
await Self.sync(user.name, password, force, options);
};
};

View File

@ -24,17 +24,22 @@ module.exports = Self => {
}
});
Self.sync = async function(userName, password, force) {
Self.sync = async function(userName, password, force, options) {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const models = Self.app.models;
const user = await models.VnUser.findOne({
fields: ['id'],
where: {name: userName}
});
const isSync = !await models.UserSync.exists(userName);
}, myOptions);
const isSync = !await models.UserSync.exists(userName, myOptions);
if (!force && isSync && user) return;
await models.AccountConfig.syncUser(userName, password);
await models.UserSync.destroyById(userName);
await models.UserSync.destroyById(userName, myOptions);
};
};

View File

@ -1,5 +1,5 @@
const app = require('vn-loopback/server/server');
const models = require('vn-loopback/server/server').models;
module.exports = Self => {
Object.assign(Self, {
@ -63,7 +63,7 @@ module.exports = Self => {
Object.assign(Self.prototype, {
async synchronizerInit() {
let mailConfig = await app.models.MailConfig.findOne({
let mailConfig = await models.MailConfig.findOne({
fields: ['domain']
});
@ -91,8 +91,6 @@ module.exports = Self => {
},
async synchronizerSyncUser(userName, password, syncGroups) {
let $ = app.models;
if (!userName) return;
userName = userName.toLowerCase();
@ -100,7 +98,7 @@ module.exports = Self => {
if (['administrator', 'root'].indexOf(userName) >= 0)
return;
let user = await $.VnUser.findOne({
let user = await models.VnUser.findOne({
where: {name: userName},
fields: [
'id',
@ -111,7 +109,7 @@ module.exports = Self => {
'sync',
'active',
'created',
'bcryptPassword',
'password',
'updated'
],
include: [
@ -138,7 +136,7 @@ module.exports = Self => {
};
if (user) {
let exists = await $.Account.exists(user.id);
let exists = await models.Account.exists(user.id);
Object.assign(info, {
hasAccount: user.active && exists,
corporateMail: `${userName}@${this.domain}`,
@ -173,30 +171,6 @@ module.exports = Self => {
async synchronizerSyncRoles() {
for (let synchronizer of this.synchronizers)
await synchronizer.syncRoles();
},
async syncUser(userName, info, password) {
if (info.user && password)
await app.models.VnUser.setPassword(info.user.id, password);
},
async getUsers(usersToSync) {
let accounts = await app.models.Account.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
where: {active: true}
}
}
});
for (let account of accounts) {
let user = account.user();
if (!user) continue;
usersToSync.add(user.name);
}
}
});
};

View File

@ -6,9 +6,6 @@
"table": "account.accountConfig"
}
},
"mixins": {
"AccountSynchronizer": {}
},
"properties": {
"id": {
"type": "number",

View File

@ -5,8 +5,7 @@ import UserError from 'core/lib/user-error';
export default class Controller extends Section {
onSynchronizeAll() {
this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
this.$http.patch(`Accounts/syncAll`)
.then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
this.$http.patch(`Accounts/syncAll`);
}
onUserSync() {

View File

@ -1,6 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('setPassword', {
Self.remoteMethod('setPassword', {
description: 'Sets the password of a non-worker client',
accepts: [
{
@ -21,14 +21,14 @@ module.exports = Self => {
}
});
Self.setPassword = async function(ctx, id, newPassword) {
Self.setPassword = async function(id, newPassword) {
const models = Self.app.models;
const isClient = await models.Client.findById(id);
const isAccount = await models.Account.findById(id);
if (isClient && !isAccount)
await models.Account.setPassword(ctx, id, newPassword);
await models.Account.setPassword(id, newPassword);
else
throw new UserError(`Modifiable password only via recovery or by an administrator`);
};

View File

@ -1,12 +1,11 @@
const models = require('vn-loopback/server/server').models;
describe('Client setPassword', () => {
const ctx = {req: {accessToken: {userId: 9}}};
it('should throw an error the setPassword target is not just a client but a worker', async() => {
let error;
try {
await models.Client.setPassword(ctx, 1, 't0pl3v3l.p455w0rd!');
await models.Client.setPassword(1, 't0pl3v3l.p455w0rd!');
} catch (e) {
error = e;
}
@ -18,7 +17,7 @@ describe('Client setPassword', () => {
let error;
try {
await models.Client.setPassword(ctx, 1101, 't0pl3v3l.p455w0rd!');
await models.Client.setPassword(1101, 't0pl3v3l.p455w0rd!');
} catch (e) {
error = e;
}

View File

@ -2,7 +2,7 @@ const UserError = require('vn-loopback/util/user-error');
const base64url = require('base64url');
module.exports = Self => {
Self.remoteMethodCtx('confirm', {
Self.remoteMethod('confirm', {
description: 'Confirms electronic payment transaction',
accessType: 'WRITE',
accepts: [
@ -30,7 +30,7 @@ module.exports = Self => {
}
});
Self.confirm = async(ctx, signatureVersion, merchantParameters, signature) => {
Self.confirm = async(signatureVersion, merchantParameters, signature) => {
const $ = Self.app.models;
let transaction;
@ -83,7 +83,7 @@ module.exports = Self => {
params['Ds_Currency'],
params['Ds_Response'],
params['Ds_ErrorCode']
], {userId: ctx.req.accessToken.userId});
]);
return true;
} catch (err) {

View File

@ -46,7 +46,7 @@ module.exports = Self => {
}
try {
let buy = await models.Buy.findOne({where: {entryFk: args.id}}, myOptions);
let buy = await models.Buy.findOne({where: {entryFk: args.id, itemFk: args.item}}, myOptions);
if (buy)
await buy.updateAttribute('printedStickers', args.printedStickers, myOptions);
else {

View File

@ -32,7 +32,7 @@ module.exports = Self => {
}
try {
await Self.rawSql(`CALL vn.invoiceInBookingMain(?)`, [id], myOptions);
await Self.rawSql(`CALL vn.invoiceIn_booking(?)`, [id], myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();

View File

@ -118,7 +118,8 @@
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.companyFk">
ng-model="$ctrl.companyFk"
on-change="$ctrl.getInvoiceDate(value)">
</vn-autocomplete>
<vn-autocomplete
url="Printers"

View File

@ -7,23 +7,29 @@ class Controller extends Section {
$onInit() {
const date = Date.vnNew();
Object.assign(this, {
maxShipped: new Date(date.getFullYear(), date.getMonth(), 0),
clientsToInvoice: 'all',
maxShipped: new Date(date.getFullYear(), date.getMonth(), 0),
clientsToInvoice: 'all',
});
this.$http.get('UserConfigs/getUserConfig')
.then(res => {
this.companyFk = res.data.companyFk;
const params = {
companyFk: this.companyFk
};
return this.$http.get('InvoiceOuts/getInvoiceDate', {params});
})
.then(res => {
this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null;
this.invoiceDate = this.minInvoicingDate;
});
}
.then(res => {
this.companyFk = res.data.companyFk;
this.getInvoiceDate(this.companyFk);
});
}
getInvoiceDate(companyFk) {
const params = { companyFk: companyFk };
this.fetchInvoiceDate(params);
}
fetchInvoiceDate(params) {
this.$http.get('InvoiceOuts/getInvoiceDate', { params })
.then(res => {
this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null;
this.invoiceDate = this.minInvoicingDate;
});
}
stopInvoicing() {
this.status = 'stopping';
@ -42,7 +48,7 @@ class Controller extends Section {
throw new UserError('Invoice date and the max date should be filled');
if (this.invoiceDate < this.maxShipped)
throw new UserError('Invoice date can\'t be less than max date');
if (this.invoiceDate.getTime() < this.minInvoicingDate.getTime())
if (this.minInvoicingDate && this.invoiceDate.getTime() < this.minInvoicingDate.getTime())
throw new UserError('Exists an invoice with a previous date');
if (!this.companyFk)
throw new UserError('Choose a valid company');

View File

@ -19,7 +19,8 @@ export default class Controller extends Section {
this.smartTableOptions = {
activeButtons: {
search: true,
}, columns: [
},
columns: [
{
field: 'isActive',
searchable: false

View File

@ -13,7 +13,6 @@ export default class Controller extends Section {
activeButtons: {
search: true
},
defaultSearch: true,
columns: [
{
field: 'warehouseFk',

View File

@ -1,6 +1,21 @@
<vn-auto-search
model="model">
</vn-auto-search>
<vn-crud-model
auto-load="true"
url="AgencyModes"
data="agencyModes">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Vehicles"
data="vehicles">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Workers/activeWithInheritedRole"
data="activeWithInheritedRole">
</vn-crud-model>
<div class="vn-w-xl">
<vn-card>
<smart-table
@ -83,7 +98,7 @@
<td>
<vn-autocomplete
ng-model="route.workerFk"
url="Workers/activeWithInheritedRole"
data="activeWithInheritedRole"
show-field="nickname"
search-function="{firstName: $search}"
value-field="id"
@ -98,7 +113,7 @@
<td expand>
<vn-autocomplete
ng-model="route.agencyModeFk"
url="AgencyModes"
data="agencyModes"
show-field="name"
value-field="id"
on-change="$ctrl.updateAttributes(route)"
@ -108,7 +123,7 @@
<td expand>
<vn-autocomplete
ng-model="route.vehicleFk"
url="Vehicles"
data="vehicles"
show-field="numberPlate"
value-field="id"
on-change="$ctrl.updateAttributes(route)"

View File

@ -26,14 +26,14 @@ module.exports = Self => {
Self.getItemsPackaging = async(id, entry) => {
return Self.rawSql(`
WITH entryTmp AS (
SELECT i.id, SUM(b.quantity) quantity
SELECT i.id, SUM(b.quantity) quantity, SUM(b.printedStickers) printedStickers
FROM vn.entry e
JOIN vn.buy b ON b.entryFk = e.id
JOIN vn.supplier s ON s.id = e.supplierFk
JOIN vn.item i ON i.id = b.itemFk
WHERE e.id = ? AND e.supplierFk = ?
GROUP BY i.id
) SELECT i.id, i.name, et.quantity, SUM(b.quantity) quantityTotal
) SELECT i.id, i.name, et.quantity, SUM(b.quantity) quantityTotal, et.printedStickers
FROM vn.buy b
JOIN vn.item i ON i.id = b.itemFk
JOIN vn.entry e ON e.id = b.entryFk

View File

@ -2,14 +2,17 @@ name: request
columns:
id: id
description: description
created: created
buyerCode: buyer
quantity: quantity
price: price
isOk: ok
response: response
saleFk: sale
ticketFk: ticket
attenderFk: attender
requesterFk: requester
itemFk: item
clientFk: client
response: response
total: total
buyed: buyed
saleFk: sale
created: created
isOk: ok
requesterFk: requester
attenderFk: attender
ticketFk: ticket

View File

@ -1,15 +1,18 @@
name: peticion
name: petición
columns:
id: id
description: descripción
created: creado
buyerCode: comprador
quantity: cantidad
price: precio
isOk: ok
response: respuesta
saleFk: línea
ticketFk: ticket
attenderFk: asistente
requesterFk: solicitante
itemFk: artículo
clientFk: cliente
response: respuesta
total: total
buyed: comprado
saleFk: línea
created: creado
isOk: ok
requesterFk: solicitante
attenderFk: asistente
ticketFk: ticket

View File

@ -24,40 +24,46 @@ module.exports = Self => {
Self.filter = async(filter, options) => {
const myOptions = {};
const conn = Self.dataSource.connector;
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmt = new ParameterizedSQL(
`SELECT
e.id,
e.ticketFk,
e.freightItemFk,
e.workerFk,
i1.name packageItemName,
e.counter,
i2.name freightItemName,
u.name userName,
e.created,
e.externalId,
i3.name packagingName,
i3.id packagingItemFk,
e.packagingFk,
es.workerFk expeditionScanWorkerFk,
su.name scannerUserName,
es.scanned,
est.description state
FROM vn.expedition e
LEFT JOIN vn.expeditionStateType est ON est.id = e.stateTypeFk
INNER JOIN vn.item i1 ON i1.id = e.freightItemFk
LEFT JOIN vn.packaging p ON p.id = e.packagingFk
LEFT JOIN vn.item i3 ON i3.id = p.itemFk
LEFT JOIN vn.item i2 ON i2.id = p.itemFk
LEFT JOIN account.user u ON u.id = e.workerFk
LEFT JOIN vn.expeditionScan es ON es.expeditionFk = e.id
LEFT JOIN account.user su ON su.id = es.workerFk
`SELECT *
FROM (
SELECT
e.id,
e.ticketFk,
e.freightItemFk,
e.workerFk,
i1.name packageItemName,
e.counter,
i2.name freightItemName,
u.name userName,
e.created,
e.externalId,
i3.name packagingName,
i3.id packagingItemFk,
e.packagingFk,
es.workerFk expeditionScanWorkerFk,
su.name scannerUserName,
es.scanned,
est.description state
FROM vn.expedition e
LEFT JOIN vn.expeditionStateType est ON est.id = e.stateTypeFk
INNER JOIN vn.item i1 ON i1.id = e.freightItemFk
LEFT JOIN vn.packaging p ON p.id = e.packagingFk
LEFT JOIN vn.item i3 ON i3.id = p.itemFk
LEFT JOIN vn.item i2 ON i2.id = p.itemFk
LEFT JOIN account.user u ON u.id = e.workerFk
LEFT JOIN vn.expeditionScan es ON es.expeditionFk = e.id
LEFT JOIN account.user su ON su.id = es.workerFk
) e
`);
stmt.merge(Self.buildSuffix(filter, 'e'));
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makeOrderBy(filter.order));
stmt.merge(conn.makeLimit(filter));
return Self.rawStmt(stmt, myOptions);
};

View File

@ -10,7 +10,7 @@ describe('expedition filter()', () => {
const filter = {where: {packagingFk: 1}};
const response = await models.Expedition.filter(filter, options);
expect(response.length).toBeGreaterThan(1);
expect(response.length).toBeGreaterThan(-1);
await tx.rollback();
} catch (e) {

View File

@ -100,5 +100,8 @@
},
"TicketConfig": {
"dataSource": "vn"
},
"PackingSiteAdvanced": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,18 @@
{
"name": "PackingSiteAdvanced",
"base": "VnModel",
"options": {
"mysql": {
"table": "packingSiteAdvanced"
}
},
"properties": {
"ticketFk": {
"id": true,
"type": "number"
},
"workerFk": {
"type": "number"
}
}
}

View File

@ -326,14 +326,8 @@ class Controller extends Section {
if (!force)
return this.$.pdfToTablet.show();
return this.$http.post(`Docuwares/${this.id}/upload`, {fileCabinet: 'deliveryNote'})
return this.$http.post(`Docuwares/upload`, {fileCabinet: 'deliveryNote', ticketIds: [this.id]})
.then(() => {
this.$.balanceCreate.amountPaid = this.ticket.totalWithVat;
this.$.balanceCreate.clientFk = this.ticket.clientFk;
this.$.balanceCreate.description = 'Albaran: ';
this.$.balanceCreate.description += this.ticket.id;
this.$.balanceCreate.show();
this.vnApp.showSuccess(this.$t('PDF sent!'));
});
}

View File

@ -304,17 +304,15 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
expect(controller.$.pdfToTablet.show).toHaveBeenCalled();
});
it('should make a query and show balance create', () => {
it('should make a query', () => {
controller.$.balanceCreate = {show: () => {}};
jest.spyOn(controller.$.balanceCreate, 'show');
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.whenPOST(`Docuwares/${ticket.id}/upload`).respond(true);
$httpBackend.whenPOST(`Docuwares/upload`).respond(true);
controller.uploadDocuware(true);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.balanceCreate.show).toHaveBeenCalled();
});
});

View File

@ -7,10 +7,12 @@
order="created DESC"
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-pa-lg">
<vn-horizontal class="header">
<vn-tool-bar class="vn-mb-md">
<vn-card class="vn-pa-lg">
<smart-table model="model"
view-config-id="expeditionIndex"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<vn-button icon="keyboard_arrow_down"
label="Move"
ng-click="moreOptions.show($event)"
@ -22,65 +24,77 @@
icon="delete"
vn-tooltip="Delete expedition">
</vn-button>
</vn-tool-bar>
<vn-one class="taxes" ng-if="$ctrl.sales.length > 0">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}</strong></p>
</vn-one>
</vn-horizontal>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="itemFk" number>Expedition</vn-th>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="packageItemName">Name</vn-th>
<vn-th field="freightItemName">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="created" expand>Created</vn-th>
<vn-th field="state" expand>State</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="expedition in expeditions">
<vn-td shrink>
<vn-check tabindex="-1"
ng-model="expedition.checked">
</vn-check>
</vn-td>
<vn-td number expand>{{expedition.id}}</vn-td>
<vn-td number>
<span
ng-class="{link: expedition.packagingItemFk}"
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)">
{{expedition.packagingFk}}
</span>
</vn-td>
<vn-td>{{::expedition.packageItemName}}</vn-td>
<vn-td>{{::expedition.freightItemName}}</vn-td>
<vn-td number>{{::expedition.counter}}</vn-td>
<vn-td expand>{{::expedition.externalId}}</vn-td>
<vn-td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td>{{::expedition.state}}</vn-td>
<vn-td>
<vn-icon-button
vn-click-stop="$ctrl.showLog(expedition)"
vn-tooltip="Status log"
icon="history">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th shrink>
<multi-check
model="model">
</multi-check>
</th>
<th field="expeditionFk" number>
<span translate>Expedition</span>
</th>
<th field="itemFk" number>
<span translate>Item</span>
</th>
<th field="packageItemName">
<span translate>Name</span>
</th>
<th field="freightItemName">
<span translate>Package Type</span>
</th>
<th field="counter" number>
<span translate>Counter</span>
</th>
<th field="externalId" number>
<span translate>externalId</span>
</th>
<th field="created" expand>
<span translate>Created</span>
</th>
<th field="state" expand>
<span translate>State</span>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="expedition in expeditions">
<td shrink>
<vn-check tabindex="-1"
ng-model="expedition.checked">
</vn-check>
</td>
<td number expand>{{expedition.id}}</td>
<td number>
<span
ng-class="{link: expedition.packagingItemFk}"
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)">
{{expedition.packagingItemFk}}
</span>
</td>
<td>{{::expedition.packageItemName}}</td>
<td>{{::expedition.freightItemName}}</td>
<td number>{{::expedition.counter}}</td>
<td expand>{{::expedition.externalId}}</td>
<td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</td>
<td>{{::expedition.state}}</td>
<td>
<vn-icon-button
vn-click-stop="$ctrl.showLog(expedition)"
vn-tooltip="Status log"
icon="history">
</vn-icon-button>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.ticket.warehouseFk",

View File

@ -6,6 +6,30 @@ class Controller extends Section {
super($element, $scope);
this.landed = Date.vnNew();
this.newRoute = null;
this.smartTableOptions = {
activeButtons: {
search: true,
shownColumns: true,
},
columns: [
{
field: 'packageItemName',
autocomplete: {
url: 'Items',
valueField: 'id',
}
}
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'expeditionFk':
return {'id': value};
case 'packageItemName':
return {'packagingItemFk': value};
}
}
get checked() {

View File

@ -152,21 +152,21 @@
</vn-data-viewer>
<div fixed-bottom-right>
<vn-vertical style="align-items: center;">
<vn-button class="round sm vn-mb-sm"
icon="print"
<vn-button class="round vn-mb-sm"
icon="install_mobile"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.setDelivered()"
ng-click="$ctrl.sendDocuware()"
vn-tooltip="Set as delivered and open delivery note(s)"
tooltip-position="left">
</vn-button>
<vn-button class="round sm vn-mb-sm"
<vn-button class="round vn-mb-sm"
icon="icon-recovery"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.openBalanceDialog()"
vn-tooltip="Payment on account..."
tooltip-position="left">
</vn-button>
<vn-button class="round sm vn-mb-sm"
<vn-button class="round vn-mb-sm"
icon="icon-invoice"
ng-click="makeInvoiceConfirmation.show()"
ng-show="$ctrl.totalChecked > 0"

View File

@ -9,28 +9,23 @@ export default class Controller extends Section {
this.vnReport = vnReport;
}
setDelivered() {
sendDocuware() {
const checkedTickets = this.checked;
let ids = [];
let ticketIds = [];
for (let ticket of checkedTickets)
ids.push(ticket.id);
ticketIds.push(ticket.id);
this.$http.post('TicketTrackings/setDelivered', ids).then(res => {
let state = res.data;
for (let ticket of checkedTickets) {
ticket.stateFk = state.id;
ticket.state = state.name;
ticket.alertLevel = state.alertLevel;
ticket.alertLevelCode = state.code;
}
this.openDeliveryNotes(ids);
});
}
openDeliveryNotes(ids) {
for (let id of ids)
this.vnReport.show(`Tickets/${id}/delivery-note-pdf`);
return this.$http.post(`Docuwares/upload`, {fileCabinet: 'deliveryNote', ticketIds})
.then(res => {
let state = res.data;
for (let ticket of checkedTickets) {
ticket.stateFk = state.id;
ticket.state = state.name;
ticket.alertLevel = state.alertLevel;
ticket.alertLevelCode = state.code;
}
});
}
openBalanceDialog() {

View File

@ -12,12 +12,14 @@ describe('Component vnTicketIndex', () => {
id: 2,
clientFk: 1,
checked: true,
totalWithVat: 20.5
totalWithVat: 20.5,
stateFk: 1
}, {
id: 3,
clientFk: 1,
checked: true,
totalWithVat: 30
totalWithVat: 30,
stateFk: 1
}];
beforeEach(ngModule('ticket'));
@ -86,18 +88,16 @@ describe('Component vnTicketIndex', () => {
});
});
describe('setDelivered()/openDeliveryNotes()', () => {
it('should perform a post to setDelivered and open tabs with the delivery notes', () => {
describe('sendDocuware()', () => {
it('should perform a post to sendDocuware and change tickets state', () => {
controller.$.model = {data: tickets, refresh: () => {}};
const newState = {id: 2};
$window.open = jest.fn();
$httpBackend.expect('POST', 'TicketTrackings/setDelivered').respond('ok');
controller.setDelivered();
$httpBackend.expect('POST', 'Docuwares/upload').respond({id: newState.id});
controller.sendDocuware();
$httpBackend.flush();
expect($window.open).toHaveBeenCalledWith(`api/Tickets/${tickets[1].id}/delivery-note-pdf`);
expect($window.open).toHaveBeenCalledWith(`api/Tickets/${tickets[2].id}/delivery-note-pdf`);
expect(controller.$.model.data[1].stateFk).toEqual(newState.id);
});
});

View File

@ -1,14 +1,14 @@
name: zone event
name: event
columns:
id: id
zoneFk: zone
type: type
dated: dated
started: started
ended: ended
started: starts
ended: ends
weekDays: week days
hour: hour
travelingDays: traveling days
price: price
bonus: bonus
m3Max: max m3
m3Max: max. m3

View File

@ -1,11 +1,11 @@
name: evento zona
name: evento
columns:
id: id
zoneFk: zona
type: tipo
dated: fecha
started: comenzado
ended: terminado
started: empieza
ended: termina
weekDays: días semana
hour: hora
travelingDays: días de viaje

View File

@ -1,5 +1,5 @@
name: zone exclusion
name: exclusion
columns:
id: id
dated: dated
dated: date
zoneFk: zone

View File

@ -1,4 +1,4 @@
name: zone exclusion
name: exclusión
columns:
id: id
dated: fecha

View File

@ -1,5 +1,7 @@
name: zone included
name: inclusion
columns:
id: id
dated: dated
zoneFk: zone
isIncluded: incluida
geoFk: localización

View File

@ -1,5 +1,7 @@
name: zona incluida
name: inclusión
columns:
id: id
dated: fecha
zoneFk: zona
isIncluded: incluida
geoFk: localización

View File

@ -1,4 +1,4 @@
name: zone warehouse
name: warehouse
columns:
id: id
warehouseFk: warehouse

View File

@ -1,4 +1,4 @@
name: almacén zona
name: almacén
columns:
id: id
warehouseFk: almacén

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.24.01",
"version": "23.26.01",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,3 @@
subject: Modified entry
title: Modified entry
description: From Warehouse the following packaging entry has been created/modified

View File

@ -0,0 +1,3 @@
subject: Entrada modificada
title: Entrada modificada
description: Desde Almacén se ha creado/modificado la siguiente entrada de embalajes

View File

@ -0,0 +1,11 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>
{{ $t('description') }}:
<a :href="url">{{ url }}</a>
</p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,16 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'modified-entry',
components: {
'email-body': emailBody.build(),
},
props: {
url: {
type: String,
required: true
}
}
};