Merge branch 'dev' into 3681-move-sign_save-to-salix
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Pau 2022-11-30 07:24:26 +01:00
commit a676351e5b
163 changed files with 67695 additions and 587 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@ module.exports = Self => {
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
await fs.unlink(dstFile);
} catch (err) {
if (err.code != 'ENOENT')
if (err.code != 'ENOENT' && dms.file)
throw err;
}

View File

@ -29,6 +29,9 @@ module.exports = Self => {
try {
const config = await models.NotificationConfig.findOne({}, myOptions);
if (!config.cleanDays) return;
const cleanDate = new Date();
cleanDate.setDate(cleanDate.getDate() - config.cleanDays);
@ -36,11 +39,11 @@ module.exports = Self => {
where: {status: {inq: status}},
created: {lt: cleanDate}
}, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
if (tx) await tx.commit();
};
};

View File

@ -1,5 +1,4 @@
const {Email} = require('vn-print');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('send', {
@ -35,7 +34,10 @@ module.exports = Self => {
include: {
relation: 'user',
scope: {
fields: ['name', 'email', 'lang']
fields: ['name', 'lang'],
include: {
relation: 'emailUser'
}
}
}
}
@ -56,7 +58,7 @@ module.exports = Self => {
for (const notificationUser of queue.notification().subscription()) {
try {
const sendParams = {
recipient: notificationUser.user().email,
recipient: notificationUser.user().emailUser().email,
lang: notificationUser.user().lang
};

View File

@ -1,4 +1,7 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const md5 = require('md5');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = Self => {
require('../methods/account/login')(Self);
@ -6,6 +9,7 @@ module.exports = Self => {
require('../methods/account/acl')(Self);
require('../methods/account/change-password')(Self);
require('../methods/account/set-password')(Self);
require('../methods/account/recover-password')(Self);
require('../methods/account/validate-token')(Self);
require('../methods/account/privileges')(Self);
@ -27,17 +31,62 @@ module.exports = Self => {
ctx.data.password = md5(ctx.data.password);
});
Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
if (!ctx.args || !ctx.args.data.email) return;
const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const url = origin.split(':');
const userId = ctx.instance.id;
const user = await models.user.findById(userId);
class Mailer {
async send(verifyOptions, cb) {
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to,
lang: ctx.req.getLocale()
};
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
}
}
const options = {
type: 'email',
to: instance.email,
from: {},
redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
template: false,
mailer: new Mailer,
host: url[1].split('/')[2],
port: url[2],
protocol: url[0],
user: Self
};
await user.verify(options);
});
Self.remoteMethod('getCurrentUserData', {
description: 'Gets the current user data',
accepts: [
{
arg: 'ctx',
type: 'Object',
type: 'object',
http: {source: 'context'}
}
],
returns: {
type: 'Object',
type: 'object',
root: true
},
http: {
@ -58,7 +107,7 @@ module.exports = Self => {
*
* @param {Integer} userId The user id
* @param {String} name The role name
* @param {Object} options Options
* @param {object} options Options
* @return {Boolean} %true if user has the role, %false otherwise
*/
Self.hasRole = async function(userId, name, options) {
@ -70,8 +119,8 @@ module.exports = Self => {
* Get all user roles.
*
* @param {Integer} userId The user id
* @param {Object} options Options
* @return {Object} User role list
* @param {object} options Options
* @return {object} User role list
*/
Self.getRoles = async(userId, options) => {
let result = await Self.rawSql(

View File

@ -40,6 +40,9 @@
"email": {
"type": "string"
},
"emailVerified": {
"type": "boolean"
},
"created": {
"type": "date"
},
@ -88,16 +91,23 @@
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
},
{
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "logout",
"property": "logout",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
},
{
"property": "validateToken",
"property": "validateToken",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",

View File

@ -4,4 +4,23 @@ module.exports = Self => {
require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self);
require('../methods/chat/sendQueued')(Self);
Self.observe('before save', async function(ctx) {
if (!ctx.isNewInstance) return;
let {message} = ctx.instance;
if (!message) return;
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
if (!parts) return;
const replacedParts = parts.map(part => {
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
});
for (const [index, part] of parts.entries())
message = message.replace(part, replacedParts[index]);
ctx.instance.message = message;
});
};

View File

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

View File

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

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

@ -0,0 +1,27 @@
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print');
module.exports = function(Self) {
Self.on('resetPasswordRequest', async function(info) {
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const user = await Self.app.models.Account.findById(info.user.id);
const params = {
recipient: info.email,
lang: user.lang,
url: `${origin}/#!/reset-password?access_token=${info.accessToken.id}`
};
const options = Object.assign({}, info.options);
for (const param in options)
params[param] = options[param];
const email = new Email(options.emailTemplate, params);
return email.send();
});
};

View File

@ -0,0 +1,2 @@
DELETE FROM `salix`.`ACL`
WHERE model = 'UserPassword';

View File

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

View File

@ -0,0 +1 @@
Alter table `vn`.`expedition` RENAME COLUMN itemFk TO itemFk__;

View File

@ -0,0 +1,8 @@
ALTER TABLE
`vn`.`client`
ADD
COLUMN `hasElectronicInvoice` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Registro de facturas mediante FACe'
AFTER
`hasInvoiceSimplified`;
-- sería más correcto hasElectronicInvoice pero ya existe un campo hasInvoiceSimplified

View File

@ -0,0 +1 @@
DROP PROCEDURE IF EXISTS `vn`.`collection_missingTrash`;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`greuge` CHANGE `userFK` `userFk` int(10) unsigned DEFAULT NULL NULL;

View File

@ -0,0 +1 @@
insert into `util`.`notification` (`id`, `name`,`description`) values (2, 'invoiceElectronic', 'A electronic invoice has been generated');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Receipt', 'balanceCompensationEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Receipt', 'balanceCompensationPdf', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,14 @@
DROP PROCEDURE IF EXISTS `vn`.`ticket_canMerge`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canMerge`(vDated DATE, vScopeDays INT, vLitersMax INT, vLinesMax INT, vWarehouseFk INT)
BEGIN
CALL vn.ticket_canbePostponed(vDated,TIMESTAMPADD(DAY, vScopeDays, vDated),vLitersMax,vLinesMax,vWarehouseFk);
END $$
DELIMITER ;
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Ticket', 'getTicketsFuture', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'merge', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,79 @@
DROP PROCEDURE IF EXISTS `vn`.`ticket_canbePostponed`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vLitersMax INT, vLinesMax INT, vWarehouseFk INT)
BEGIN
/**
* Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro
*
* @param vOriginDated Fecha en cuestión
* @param vFutureDated Fecha en el futuro a sondear
* @param vLitersMax Volumen máximo de los tickets a catapultar
* @param vLinesMax Número máximo de lineas de los tickets a catapultar
* @param vWarehouseFk Identificador de vn.warehouse
*/
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT sv.ticketFk id,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
st.name state,
sub2.id ticketFuture,
t.landed originETD,
sub2.landed destETD,
sub2.iptd tfIpt,
sub2.state tfState,
t.clientFk,
t.warehouseFk,
ts.alertLevel,
t.shipped,
sub2.shipped tfShipped,
t.workerFk,
st.code code,
sub2.code tfCode
FROM vn.saleVolume sv
JOIN vn.sale s ON s.id = sv.saleFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticket t ON t.id = sv.ticketFk
JOIN vn.address a ON a.id = t.addressFk
JOIN vn.province p ON p.id = a.provinceFk
JOIN vn.country c ON c.id = p.countryFk
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.alertLevel al ON al.id = ts.alertLevel
LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id
LEFT JOIN (
SELECT *
FROM (
SELECT
t.addressFk ,
t.id,
t.landed,
t.shipped,
st.name state,
st.code code,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
WHERE t.shipped BETWEEN vFutureDated
AND util.dayend(vFutureDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) sub
GROUP BY sub.addressFk
) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id
WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated)
AND t.warehouseFk = vWarehouseFk
AND al.code = 'FREE'
AND tp.ticketFk IS NULL
GROUP BY sv.ticketFk
HAVING liters <= IFNULL(vLitersMax, 9999) AND `lines` <= IFNULL(vLinesMax, 9999) AND ticketFuture;
END$$
DELIMITER ;

View File

@ -0,0 +1,6 @@
UPDATE
`vn`.`client`
SET
hasElectronicInvoice = TRUE
WHERE
businessTypeFk = 'officialOrganism';

View File

@ -0,0 +1 @@
Delete this file

View File

@ -686,7 +686,10 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE()),
(25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE());
(27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()),
(30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE());
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES
@ -918,7 +921,7 @@ INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`)
(3, 'Perdida', 'LOST');
INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`)
INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk__`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`)
VALUES
(1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'),
(2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL),
@ -984,7 +987,10 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric
(30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()),
(31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, util.VN_CURDATE()),
(32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, util.VN_CURDATE()),
(33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE());
(33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE()),
(34, 4, 28, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()),
(35, 4, 29, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()),
(36, 4, 30, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE());
INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`)
VALUES
@ -1853,7 +1859,8 @@ INSERT INTO `vn`.`receipt`(`id`, `invoiceFk`, `amountPaid`, `payed`, `workerFk`,
(1, 'Cobro web', 100.50, util.VN_CURDATE(), 9, 1, 1101, util.VN_CURDATE(), 442, 1),
(2, 'Cobro web', 200.50, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 9, 1, 1101, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 442, 1),
(3, 'Cobro en efectivo', 300.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 9, 1, 1102, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 442, 0),
(4, 'Cobro en efectivo', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 1, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0);
(4, 'Cobro en efectivo', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 1, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0),
(5, 'Compensación', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 3, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0);
INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`)
VALUES
@ -2557,10 +2564,6 @@ UPDATE `vn`.`route`
UPDATE `vn`.`route`
SET `invoiceInFk`=2
WHERE `id`=2;
INSERT INTO `bs`.`salesPerson` (`workerFk`, `year`, `month`, `portfolioWeight`)
VALUES
(18, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 807.23),
(19, YEAR(util.VN_CURDATE()), MONTH(util.VN_CURDATE()), 34.40);
INSERT INTO `bs`.`sale` (`saleFk`, `amount`, `dated`, `typeFk`, `clientFk`)
VALUES
@ -2701,7 +2704,7 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`)
VALUES
(1, 9);
INSERT INTO `vn`.`productionConfig` (`isPreviousPreparationRequired`, `ticketPrintedMax`, `ticketTrolleyMax`, `rookieDays`, `notBuyingMonths`, `id`, `isZoneClosedByExpeditionActivated`, `maxNotReadyCollections`, `minTicketsToCloseZone`, `movingTicketDelRoute`, `defaultZone`, `defautlAgencyMode`, `hasUniqueCollectionTime`, `maxCollectionWithoutUser`, `pendingCollectionsOrder`, `pendingCollectionsAge`)
VALUES
(0, 8, 80, 0, 0, 1, 0, 15, 25, -1, 697, 1328, 0, 1, 8, 6);
@ -2725,3 +2728,7 @@ UPDATE `account`.`user`
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
INSERT INTO `vn`.`ticketLog` (`id`, `originFk`, `userFk`, `action`, `changedModel`, `oldInstance`, `newInstance`, `changedModelId`)
VALUES
(1, 1, 9, 'insert', 'Ticket', '{}', '{"clientFk":1, "nickname": "Bat cave"}', 1);

View File

@ -19,6 +19,7 @@
-- Current Database: `account`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `account` /*!40100 DEFAULT CHARACTER SET utf8mb3 */;
USE `account`;
@ -81043,4 +81044,3 @@ USE `vncontrol`;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2022-09-16 10:44:31

View File

@ -29,6 +29,11 @@ export default {
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
},
recoverPassword: {
recoverPasswordButton: 'vn-login a[ui-sref="recoverPassword"]',
email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]',
sendEmailButton: 'vn-recover-password vn-submit',
},
accountIndex: {
addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',
@ -324,7 +329,8 @@ export default {
anyBalanceLine: 'vn-client-balance-index vn-tbody > vn-tr',
firstLineBalance: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(8)',
firstLineReference: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable',
firstLineReferenceInput: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable > div > field > vn-textfield'
firstLineReferenceInput: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable > div > field > vn-textfield',
compensationButton: 'vn-client-balance-index vn-icon-button[vn-dialog="send_compensation"]'
},
webPayment: {
confirmFirstPaymentButton: 'vn-client-web-payment vn-tr:nth-child(1) vn-icon-button[icon="done_all"]',
@ -727,6 +733,34 @@ export default {
saveImport: 'button[response="accept"]',
anyDocument: 'vn-ticket-dms-index > vn-data-viewer vn-tbody vn-tr'
},
ticketFuture: {
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
originDated: 'vn-date-picker[label="Origin ETD"]',
futureDated: 'vn-date-picker[label="Destination ETD"]',
shipped: 'vn-date-picker[label="Origin date"]',
tfShipped: 'vn-date-picker[label="Destination date"]',
linesMax: 'vn-textfield[label="Max Lines"]',
litersMax: 'vn-textfield[label="Max Liters"]',
ipt: 'vn-autocomplete[label="Origin IPT"]',
tfIpt: 'vn-autocomplete[label="Destination IPT"]',
tableIpt: 'vn-autocomplete[name="ipt"]',
tableTfIpt: 'vn-autocomplete[name="tfIpt"]',
state: 'vn-autocomplete[label="Origin Grouped State"]',
tfState: 'vn-autocomplete[label="Destination Grouped State"]',
warehouseFk: 'vn-autocomplete[label="Warehouse"]',
problems: 'vn-check[label="With problems"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Future tickets"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
multiCheck: 'vn-multi-check',
tableId: 'vn-textfield[name="id"]',
tableTfId: 'vn-textfield[name="ticketFuture"]',
tableLiters: 'vn-textfield[name="litersMax"]',
tableLines: 'vn-textfield[name="linesMax"]',
submit: 'vn-submit[label="Search"]',
table: 'tbody > tr:not(.empty-rows)'
},
createStateView: {
state: 'vn-autocomplete[ng-model="$ctrl.stateFk"]',
worker: 'vn-autocomplete[ng-model="$ctrl.workerFk"]',
@ -1010,6 +1044,17 @@ export default {
booked: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.booked"]',
currency: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.currencyFk"]',
company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]',
dms: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"]',
download: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.prepend > prepend > vn-icon-button',
edit: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.append > append > vn-icon-button[icon="edit"]',
create: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.append > append > vn-icon-button[icon="add_circle"]',
reference: 'vn-textfield[ng-model="$ctrl.dms.reference"]',
companyId: 'vn-autocomplete[ng-model="$ctrl.dms.companyId"]',
warehouseId: 'vn-autocomplete[ng-model="$ctrl.dms.warehouseId"]',
dmsTypeId: 'vn-autocomplete[ng-model="$ctrl.dms.dmsTypeId"]',
description: 'vn-textarea[ng-model="$ctrl.dms.description"]',
inputFile: 'vn-input-file[ng-model="$ctrl.dms.files"]',
confirm: 'button[response="accept"]',
save: 'vn-invoice-in-basic-data button[type=submit]'
},
invoiceInTax: {

View File

@ -0,0 +1,40 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Login path', async() => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.waitForState('recoverPassword');
});
afterAll(async() => {
await browser.close();
});
it('should not throw error if not exist user', async() => {
await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com');
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Notification sent!');
});
it('should send email', async() => {
await page.waitForState('login');
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com');
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
const message = await page.waitForSnackbar();
await page.waitForState('login');
expect(message.text).toContain('Notification sent!');
});
});

View File

@ -0,0 +1,27 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Client Send balance compensation', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
await page.accessToSearchResult('Clark Kent');
await page.accessToSection('client.card.balance.index');
});
afterAll(async() => {
await browser.close();
});
it(`should click on send compensation button`, async() => {
await page.autocompleteSearch(selectors.clientBalance.company, 'VNL');
await page.waitToClick(selectors.clientBalance.compensationButton);
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Notification sent!');
});
});

View File

@ -127,8 +127,8 @@ describe('Item regularize path', () => {
await page.waitForState('ticket.index');
});
it('should search for the ticket with id 28 once again', async() => {
await page.accessToSearchResult('28');
it('should search for the ticket with id 31 once again', async() => {
await page.accessToSearchResult('31');
await page.waitForState('ticket.card.summary');
});

View File

@ -291,7 +291,7 @@ describe('Ticket Edit sale path', () => {
it('should confirm the transfered quantity is the correct one', async() => {
const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText');
expect(result).toContain('10');
expect(result).toContain('20');
});
it('should go back to the original ticket sales section', async() => {

View File

@ -0,0 +1,264 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Ticket Future path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'ticket');
await page.accessToSection('ticket.future');
});
afterAll(async() => {
await browser.close();
});
const now = new Date();
const tomorrow = new Date(now.getDate() + 1);
it('should show errors snackbar because of the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit);
let message = await page.waitForSnackbar();
expect(message.text).toContain('warehouseFk is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.litersMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('litersMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.linesMax);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('linesMax is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('futureDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.originDated);
await page.waitToClick(selectors.ticketFuture.submit);
message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument');
});
it('should search with the required data', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the origin shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, now);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the origin shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.shipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the destination shipped today', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.pickDate(selectors.ticketFuture.tfShipped, now);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search with the destination shipped tomorrow', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the origin IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.clearInput(selectors.ticketFuture.tfShipped);
await page.clearInput(selectors.ticketFuture.ipt);
await page.clearInput(selectors.ticketFuture.tfIpt);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.tfState);
await page.autocompleteSearch(selectors.ticketFuture.ipt, 'Horizontal');
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the destination IPT', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.clearInput(selectors.ticketFuture.tfShipped);
await page.clearInput(selectors.ticketFuture.ipt);
await page.clearInput(selectors.ticketFuture.tfIpt);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.tfState);
await page.autocompleteSearch(selectors.ticketFuture.tfIpt, 'Horizontal');
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
});
it('should search with the origin grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.clearInput(selectors.ticketFuture.tfShipped);
await page.clearInput(selectors.ticketFuture.ipt);
await page.clearInput(selectors.ticketFuture.tfIpt);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.tfState);
await page.autocompleteSearch(selectors.ticketFuture.state, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 3);
});
it('should search with the destination grouped state', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.clearInput(selectors.ticketFuture.tfShipped);
await page.clearInput(selectors.ticketFuture.ipt);
await page.clearInput(selectors.ticketFuture.tfIpt);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.tfState);
await page.autocompleteSearch(selectors.ticketFuture.tfState, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 0);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.shipped);
await page.clearInput(selectors.ticketFuture.tfShipped);
await page.clearInput(selectors.ticketFuture.ipt);
await page.clearInput(selectors.ticketFuture.tfIpt);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.tfState);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an ID Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableId, '13');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 2);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an ID Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableTfId, '12');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an IPT Origin', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with an IPT Destination', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with especified Lines', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, '0');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLines, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should search in smart-table with especified Liters', async() => {
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, '0');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 1);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.write(selectors.ticketFuture.tableLiters, '28');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketFuture.table, 5);
await page.waitToClick(selectors.ticketFuture.tableButtonSearch);
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.table, 4);
});
it('should check the three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
});
});

View File

@ -4,6 +4,7 @@ import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn basic data path', () => {
let browser;
let page;
let newDms;
beforeAll(async() => {
browser = await getBrowser();
@ -24,6 +25,8 @@ describe('InvoiceIn basic data path', () => {
await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura');
await page.clearInput(selectors.invoiceInBasicData.supplierRef);
await page.write(selectors.invoiceInBasicData.supplierRef, '9999');
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '2');
await page.pickDate(selectors.invoiceInBasicData.bookEntried, now);
await page.pickDate(selectors.invoiceInBasicData.booked, now);
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'USD');
@ -61,4 +64,141 @@ describe('InvoiceIn basic data path', () => {
expect(result).toEqual('ORN');
});
it(`should confirm the invoiceIn dms was edited`, async() => {
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
expect(result).toEqual('2');
});
it(`should create a new invoiceIn dms and save the changes`, async() => {
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.waitToClick(selectors.invoiceInBasicData.create);
await page.clearInput(selectors.invoiceInBasicData.reference);
await page.write(selectors.invoiceInBasicData.reference, 'New Dms');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
let message = await page.waitForSnackbar();
expect(message.text).toContain('The company can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.companyId);
await page.autocompleteSearch(selectors.invoiceInBasicData.companyId, 'VNL');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The warehouse can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.warehouseId);
await page.autocompleteSearch(selectors.invoiceInBasicData.warehouseId, 'Warehouse One');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The DMS Type can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.dmsTypeId);
await page.autocompleteSearch(selectors.invoiceInBasicData.dmsTypeId, 'Ticket');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The description can\'t be empty');
await page.waitToClick(selectors.invoiceInBasicData.description);
await page.write(selectors.invoiceInBasicData.description, 'Dms without edition.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The files can\'t be empty');
let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/assets/thermograph.jpeg`;
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.waitToClick(selectors.invoiceInBasicData.inputFile)
]);
await fileChooser.accept([filePath]);
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
newDms = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
});
it(`should confirm the invoiceIn was edited with the new dms`, async() => {
await page.reloadSection('invoiceIn.card.basicData');
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
expect(result).toEqual(newDms);
});
it(`should edit the invoiceIn`, async() => {
await page.waitToClick(selectors.invoiceInBasicData.edit);
await page.clearInput(selectors.invoiceInBasicData.reference);
await page.write(selectors.invoiceInBasicData.reference, 'Dms Edited');
await page.clearInput(selectors.invoiceInBasicData.companyId);
await page.autocompleteSearch(selectors.invoiceInBasicData.companyId, 'CCs');
await page.clearInput(selectors.invoiceInBasicData.warehouseId);
await page.autocompleteSearch(selectors.invoiceInBasicData.warehouseId, 'Algemesi');
await page.clearInput(selectors.invoiceInBasicData.dmsTypeId);
await page.autocompleteSearch(selectors.invoiceInBasicData.dmsTypeId, 'Basura');
await page.waitToClick(selectors.invoiceInBasicData.description);
await page.write(selectors.invoiceInBasicData.description, ' Nevermind, now is edited.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
let message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should confirm the new dms has been edited`, async() => {
await page.reloadSection('invoiceIn.card.basicData');
await page.waitToClick(selectors.invoiceInBasicData.edit);
const reference = await page
.waitToGetProperty(selectors.invoiceInBasicData.reference, 'value');
const companyId = await page
.waitToGetProperty(selectors.invoiceInBasicData.companyId, 'value');
const warehouseId = await page
.waitToGetProperty(selectors.invoiceInBasicData.warehouseId, 'value');
const dmsTypeId = await page
.waitToGetProperty(selectors.invoiceInBasicData.dmsTypeId, 'value');
const description = await page
.waitToGetProperty(selectors.invoiceInBasicData.description, 'value');
expect(reference).toEqual('Dms Edited');
expect(companyId).toEqual('CCs');
expect(warehouseId).toEqual('Algemesi');
expect(dmsTypeId).toEqual('Basura');
expect(description).toEqual('Dms without edition. Nevermind, now is edited.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
});
it(`should disable edit and download if dms doesn't exists, and set back the original dms`, async() => {
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '9999');
await page.waitForSelector(`${selectors.invoiceInBasicData.download}.disabled`);
await page.waitForSelector(`${selectors.invoiceInBasicData.edit}.disabled`);
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '1');
await page.waitToClick(selectors.invoiceInBasicData.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
});

View File

@ -26,6 +26,7 @@ export default class Searchbar extends Component {
this.autoState = true;
this.separateIndex = true;
this.entityState = 'card.summary';
this.isIndex = false;
this.deregisterCallback = this.$transitions.onSuccess(
{}, transition => this.onStateChange(transition));
@ -102,6 +103,9 @@ export default class Searchbar extends Component {
filter = {};
}
let stateParts = this.$state.current.name.split('.');
this.isIndex = stateParts[1] == 'index';
this.doSearch(filter, 'state');
}
@ -198,7 +202,7 @@ export default class Searchbar extends Component {
}
doSearch(filter, source) {
if (filter === this.filter && source != 'state') return;
if (filter === this.filter && !this.isIndex) return;
let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data));

View File

@ -12,9 +12,10 @@ export default class Component extends EventEmitter {
* @param {HTMLElement} $element The main component element
* @param {$rootScope.Scope} $scope The element scope
* @param {Function} $transclude The transclusion function
* @param {Function} $location The location function
*/
constructor($element, $scope, $transclude) {
super();
constructor($element, $scope, $transclude, $location) {
super($element, $scope, $transclude, $location);
this.$ = $scope;
if (!$element) return;
@ -164,7 +165,7 @@ export default class Component extends EventEmitter {
$transclude.$$boundTransclude.$$slots[slot];
}
}
Component.$inject = ['$element', '$scope'];
Component.$inject = ['$element', '$scope', '$location', '$state'];
/*
* Automatically adds the most used services to the prototype, so they are

View File

@ -23,7 +23,10 @@ export default class Auth {
initialize() {
let criteria = {
to: state => state.name != 'login'
to: state => {
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return !outLayout.some(ol => ol == state.name);
}
};
this.$transitions.onStart(criteria, transition => {
if (this.loggedIn)

View File

@ -1,9 +1,8 @@
<vn-layout
ng-if="$ctrl.showLayout">
</vn-layout>
<ui-view
name="login"
<vn-out-layout
ng-if="!$ctrl.showLayout">
</ui-view>
</vn-out-layout>
<vn-snackbar vn-id="snackbar"></vn-snackbar>
<vn-debug-info></vn-debug-info>

View File

@ -9,13 +9,20 @@ import Component from 'core/lib/component';
* @property {SideMenu} rightMenu The left menu, if it's present
*/
export default class App extends Component {
constructor($element, $, $location, $state) {
super($element, $, $location, $state);
this.$location = $location;
this.$state = $state;
}
$postLink() {
this.vnApp.logger = this;
}
get showLayout() {
let state = this.$state.current.name;
return state && state != 'login';
const state = this.$state.current.name || this.$location.$$path.substring(1).replace('/', '.');
const outLayout = ['login', 'recoverPassword', 'resetPassword'];
return state && !outLayout.some(ol => ol == state);
}
$onDestroy() {

View File

@ -5,7 +5,10 @@ import './descriptor-popover';
import './home/home';
import './layout';
import './left-menu/left-menu';
import './login/index';
import './login/login';
import './login/recover-password';
import './login/reset-password';
import './module-card';
import './module-main';
import './side-menu/side-menu';

View File

@ -33,16 +33,18 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
if (!value) return;
if (this.logs) {
this.logs.forEach(log => {
log.oldProperties = this.getInstance(log.oldInstance);
log.newProperties = this.getInstance(log.newInstance);
});
}
const validations = window.validations;
value.forEach(log => {
const locale = validations[log.changedModel] && validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
log.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale);
});
}
getInstance(instance) {
getInstance(instance, locale) {
const properties = [];
let validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/;
@ -51,7 +53,8 @@ export default class Controller extends Section {
if (validDate.test(instance[property]))
instance[property] = new Date(instance[property]).toLocaleString('es-ES');
properties.push({key: property, value: instance[property]});
const key = locale[property] || property;
properties.push({key, value: instance[property]});
});
return properties;
}

View File

@ -0,0 +1,6 @@
<div class="box">
<img src="./logo.svg"/>
<form name="form">
<ui-view></ui-view>
</form>
</div>

View File

@ -0,0 +1,16 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
export default class OutLayout extends Component {
constructor($element, $scope) {
super($element, $scope);
}
}
OutLayout.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnOutLayout', {
template: require('./index.html'),
controller: OutLayout
});

View File

@ -1,4 +1,8 @@
User: User
Password: Password
Do not close session: Do not close session
Enter: Enter
Enter: Enter
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

@ -1,4 +1,16 @@
User: Usuario
Password: Contraseña
Email: Correo electrónico
Do not close session: No cerrar sesión
Enter: Entrar
Enter: Entrar
I do not remember my password: No recuerdo mi contraseña
Recover password: Recuperar contraseña
We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña
Notification sent!: ¡Notificación enviada!
Reset password: Restrablecer contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseña
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

@ -1,27 +1,27 @@
<div class="box">
<img src="./logo.svg"/>
<form name="form" ng-submit="$ctrl.submit()">
<vn-textfield
label="User"
ng-model="$ctrl.user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.password"
type="password">
</vn-textfield>
<vn-check
label="Do not close session"
ng-model="$ctrl.remember"
name="remember">
</vn-check>
<div class="footer">
<vn-submit label="Enter"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
</div>
</form>
<vn-textfield
label="User"
ng-model="$ctrl.user"
vn-id="userField"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.password"
type="password">
</vn-textfield>
<vn-check
label="Do not close session"
ng-model="$ctrl.remember"
name="remember">
</vn-check>
<div class="footer">
<vn-submit label="Enter" ng-click="$ctrl.submit()"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>
<div class="vn-pt-lg">
<a ui-sref="recoverPassword" translate>
I do not remember my password
</a>
</div>
</div>

View File

@ -0,0 +1,17 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Recover password</h5>
<vn-textfield
label="Email"
ng-model="$ctrl.email"
vn-focus>
</vn-textfield>
<div
class="text-secondary"
translate>
We will sent you an email to recover your password
</div>
<div class="footer">
<vn-submit label="Recover 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,37 @@
import ngModule from '../../module';
import './style.scss';
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state
});
}
goToLogin() {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('login');
}
submit() {
const params = {
email: this.email
};
this.$http.post('Accounts/recoverPassword', params)
.then(() => {
this.goToLogin();
});
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state'];
ngModule.vnComponent('vnRecoverPassword', {
template: require('./recover-password.html'),
controller: Controller
});

View File

@ -0,0 +1,19 @@
<h5 class="vn-mb-md vn-mt-lg" translate>Reset password</h5>
<vn-textfield
label="New password"
ng-model="$ctrl.newPassword"
type="password"
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
vn-focus>
</vn-textfield>
<vn-textfield
label="Repeat password"
ng-model="$ctrl.repeatPassword"
type="password">
</vn-textfield>
<div class="footer">
<vn-submit label="Reset 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,48 @@
import ngModule from '../../module';
import './style.scss';
export default class Controller {
constructor($scope, $element, $http, vnApp, $translate, $state, $location) {
Object.assign(this, {
$scope,
$element,
$http,
vnApp,
$translate,
$state,
$location
});
}
$onInit() {
this.$http.get('UserPasswords/findOne')
.then(res => {
this.passRequirements = res.data;
});
}
submit() {
if (!this.newPassword)
throw new UserError(`You must enter a new password`);
if (this.newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`);
const headers = {
Authorization: this.$location.$$search.access_token
};
const newPassword = this.newPassword;
this.$http.post('users/reset-password', {newPassword}, {headers})
.then(() => {
this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
this.$state.go('login');
});
}
}
Controller.$inject = ['$scope', '$element', '$http', 'vnApp', '$translate', '$state', '$location'];
ngModule.vnComponent('vnResetPassword', {
template: require('./reset-password.html'),
controller: Controller
});

View File

@ -1,6 +1,31 @@
@import "variables";
vn-login {
vn-login,
vn-reset-password,
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;
}
}
}
vn-out-layout{
position: absolute;
height: 100%;
width: 100%;
@ -39,28 +64,17 @@ vn-login {
white-space: inherit;
}
}
& > .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;
}
}
}
h5{
color: $color-primary;
}
.text-secondary{
text-align: center;
padding-bottom: 16px;
}
}
@media screen and (max-width: 600px) {
@ -71,4 +85,8 @@ vn-login {
box-shadow: none;
}
}
a{
color: $color-primary;
}
}

View File

@ -112,7 +112,7 @@ function $exceptionHandler(vnApp, $window, $state, $injector) {
switch (exception.status) {
case 401:
if ($state.current.name != 'login') {
if (!$state.current.name.includes('login')) {
messageT = 'Session has expired';
let params = {continue: $window.location.hash};
$state.go('login', params);

View File

@ -9,9 +9,17 @@ function config($stateProvider, $urlRouterProvider) {
.state('login', {
url: '/login?continue',
description: 'Login',
views: {
login: {template: '<vn-login></vn-login>'}
}
template: '<vn-login></vn-login>'
})
.state('recoverPassword', {
url: '/recover-password',
description: 'Recover-password',
template: '<vn-recover-password>asd</vn-recover-password>'
})
.state('resetPassword', {
url: '/reset-password',
description: 'Reset-password',
template: '<vn-reset-password></vn-reset-password>'
})
.state('home', {
url: '/',

View File

@ -1,3 +1,5 @@
const path = require('path');
const fs = require('fs');
module.exports = Self => {
Self.remoteMethod('modelInfo', {
@ -19,6 +21,42 @@ module.exports = Self => {
}
});
const modelsLocale = new Map();
const modulesDir = path.resolve(`${__dirname}/../../../../modules`);
const modules = fs.readdirSync(modulesDir);
for (const mod of modules) {
const modelsDir = path.join(modulesDir, mod, `back/locale`);
if (!fs.existsSync(modelsDir)) continue;
const models = fs.readdirSync(modelsDir);
for (const model of models) {
const localeDir = path.join(modelsDir, model);
const localeFiles = fs.readdirSync(localeDir);
let modelName = model.charAt(0).toUpperCase() + model.substring(1);
modelName = modelName.replace(/-\w/g, match => {
return match.charAt(1).toUpperCase();
});
const modelLocale = new Map();
modelsLocale.set(modelName, modelLocale);
for (const localeFile of localeFiles) {
const localePath = path.join(localeDir, localeFile);
const match = localeFile.match(/^([a-z]+)\.yml$/);
if (!match) {
console.warn(`Skipping wrong model locale file: ${localeFile}`);
continue;
}
const translations = require(localePath);
modelLocale.set(match[1], translations);
}
}
}
Self.modelInfo = async function(ctx) {
let json = {};
let models = Self.app.models;
@ -49,9 +87,14 @@ module.exports = Self => {
jsonValidations[fieldName] = jsonField;
}
const modelLocale = modelsLocale.get(modelName);
const lang = ctx.req.getLocale();
const locale = modelLocale && modelLocale.get(lang);
json[modelName] = {
properties: model.definition.rawProperties,
validations: jsonValidations
validations: jsonValidations,
locale
};
}

View File

@ -131,11 +131,18 @@
"Fichadas impares": "Odd signs",
"Descanso diario 9h.": "Daily rest 9h.",
"Descanso semanal 36h. / 72h.": "Weekly rest 36h. / 72h.",
"Verify email": "Verify email",
"Click on the following link to verify this email. If you haven't requested this email, just ignore it": "Click on the following link to verify this email. If you haven't requested this email, just ignore it",
"Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege",
"You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production"
"Email verify": "Email verify",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"Receipt's bank was not found": "Receipt's bank was not found",
"This receipt was not compensated": "This receipt was not compensated",
"Client's email was not found": "Client's email was not found"
}

View File

@ -247,8 +247,14 @@
"Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*",
"You don't have grant privilege": "No tienes privilegios para dar privilegios",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) fusionado con [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio"
"Empty data source": "Origen de datos vacio",
"Email verify": "Correo de verificación",
"Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment",
"Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente"
}
>>>>>>> dev

View File

@ -113,4 +113,4 @@
"application/x-7z-compressed"
]
}
}
}

View File

@ -30,5 +30,13 @@
"type": "number",
"required": true
}
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
$onInit() {
if (this.$params.emailConfirmed)
this.vnApp.showSuccess(this.$t('Email verified successfully!'));
}
onSubmit() {
this.$.watcher.submit()
.then(() => this.card.reload());

View File

@ -0,0 +1 @@
Email verified successfully!: Correo verificado correctamente!

View File

@ -74,7 +74,7 @@
}
},
{
"url": "/basic-data",
"url": "/basic-data?emailConfirmed",
"state": "account.card.basicData",
"component": "vn-user-basic-data",
"description": "Basic data",

View File

@ -1,75 +0,0 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client updatePortfolio', () => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'},
[`__`]: value => {
return value;
}
}
}
};
beforeAll(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should update the portfolioWeight when the salesPerson of a client changes', async() => {
const clientId = 1108;
const salesPersonId = 18;
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const expectedResult = 841.63;
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('salesPersonFk', salesPersonId, options);
await models.Client.updatePortfolio(options);
const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `;
const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options);
expect(salesPerson.portfolioWeight).toEqual(expectedResult);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => {
const clientId = 1107;
const salesPersonId = 19;
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const expectedResult = 34.40;
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('salesPersonFk', null, options);
await models.Client.updatePortfolio();
const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `;
const [salesPerson] = await models.Client.rawSql(portfolioQuery);
expect(salesPerson.portfolioWeight).toEqual(expectedResult);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -99,6 +99,10 @@ module.exports = Self => {
{
arg: 'hasIncoterms',
type: 'boolean'
},
{
arg: 'hasElectronicInvoice',
type: 'boolean'
}
],
returns: {
@ -122,19 +126,15 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
const client = await models.Client.findById(clientId, null, myOptions);
if (!isSalesAssistant && client.isTaxDataChecked)
throw new UserError(`Not enough privileges to edit a client with verified data`);
// Sage data validation
const taxDataChecked = args.isTaxDataChecked;
const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk;
@ -143,7 +143,6 @@ module.exports = Self => {
if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`);
if (args.despiteOfClient) {
const logRecord = {
originFk: clientId,
@ -158,7 +157,6 @@ module.exports = Self => {
await models.ClientLog.create(logRecord, myOptions);
}
// Remove unwanted properties
delete args.ctx;
delete args.id;

View File

@ -1,25 +0,0 @@
module.exports = function(Self) {
Self.remoteMethodCtx('updatePortfolio', {
description: 'Update salesPeson potfolio weight',
accessType: 'READ',
accepts: [],
returns: {
type: 'Object',
root: true
},
http: {
path: `/updatePortfolio`,
verb: 'GET'
}
});
Self.updatePortfolio = async options => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
query = `CALL bs.salesPerson_updatePortfolio()`;
return Self.rawSql(query, null, myOptions);
};
};

View File

@ -1,4 +1,5 @@
const {Email} = require('vn-print');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('balanceCompensationEmail', {
@ -10,7 +11,7 @@ module.exports = Self => {
type: 'Number',
required: true,
description: 'The receipt id',
http: { source: 'path' }
http: {source: 'path'}
}
],
returns: {
@ -23,18 +24,29 @@ module.exports = Self => {
}
});
Self.balanceCompensationEmail = async (ctx, id) => {
Self.balanceCompensationEmail = async(ctx, id) => {
const models = Self.app.models;
const receipt = await models.Receipt.findById(id, {fields: ['clientFk']});
const client = await models.Client.findById(receipt.clientFk, {fields:['email']});
const receipt = await models.Receipt.findById(id, {fields: ['clientFk', 'bankFk']});
const email = new Email('balance-compensation', {
lang: ctx.req.getLocale(),
recipient: client.email+',administracion@verdnatura.es',
id
});
const bank = await models.Bank.findById(receipt.bankFk);
if (!bank)
throw new UserError(`Receipt's bank was not found`);
return email.send();
const accountingType = await models.AccountingType.findById(bank.accountingTypeFk);
if (!(accountingType && accountingType.code == 'compensation'))
throw new UserError(`This receipt was not compensated`);
const client = await models.Client.findById(receipt.clientFk, {fields: ['email']});
if (!client.email)
throw new UserError(`Client's email was not found`);
else {
const email = new Email('balance-compensation', {
lang: ctx.req.getLocale(),
recipient: client.email + ',administracion@verdnatura.es',
id
});
return email.send();
}
};
};

View File

@ -42,7 +42,7 @@ module.exports = Self => {
const stmt = new ParameterizedSQL(
`SELECT * FROM (
SELECT
SELECT
r.id,
r.isConciliate,
r.payed,
@ -56,13 +56,16 @@ module.exports = Self => {
u.name userName,
r.clientFk,
FALSE hasPdf,
FALSE isInvoice
FALSE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation
FROM vn.receipt r
LEFT JOIN vn.worker w ON w.id = r.workerFk
LEFT JOIN account.user u ON u.id = w.userFk
JOIN vn.company c ON c.id = r.companyFk
JOIN vn.accounting a ON a.id = r.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk
WHERE r.clientFk = ? AND r.companyFk = ?
UNION ALL
UNION ALL
SELECT
i.id,
TRUE,
@ -77,9 +80,12 @@ module.exports = Self => {
NULL,
i.clientFk,
i.hasPdf,
TRUE isInvoice
TRUE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation
FROM vn.invoiceOut i
JOIN vn.company c ON c.id = i.companyFk
JOIN vn.accounting a ON a.id = i.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk
WHERE i.clientFk = ? AND i.companyFk = ?
ORDER BY payed DESC, created DESC
) t ORDER BY payed DESC, created DESC`,

View File

@ -22,7 +22,6 @@ module.exports = Self => {
require('../methods/client/summary')(Self);
require('../methods/client/updateAddress')(Self);
require('../methods/client/updateFiscalData')(Self);
require('../methods/client/updatePortfolio')(Self);
require('../methods/client/updateUser')(Self);
require('../methods/client/uploadFile')(Self);
require('../methods/client/campaignMetricsPdf')(Self);

View File

@ -142,7 +142,11 @@
},
"salesPersonFk": {
"type": "number"
},
"hasElectronicInvoice": {
"type": "boolean"
}
},
"relations": {
"account": {

View File

@ -1,6 +1,21 @@
const LoopBackContext = require('loopback-context');
module.exports = function(Self) {
require('../methods/greuge/sumAmount')(Self);
Self.observe('before save', function(ctx, next) {
const loopBackContext = LoopBackContext.getCurrentContext();
let userFk = loopBackContext.active.accessToken.userId;
if (ctx.instance)
ctx.instance.userFk = userFk;
else
ctx.data.userFk = userFk;
next();
});
Self.validatesLengthOf('description', {
max: 45,
message: 'Description should have maximum of 45 characters'

View File

@ -2,9 +2,9 @@
"name": "Greuge",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client",
"showField": "description"
"model": "ClientLog",
"relation": "client",
"showField": "description"
},
"options": {
"mysql": {
@ -35,7 +35,6 @@
"type": "number",
"required": true
}
},
"relations": {
"client": {
@ -52,6 +51,11 @@
"type": "belongsTo",
"model": "GreugeType",
"foreignKey": "greugeTypeFk"
},
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -121,7 +121,7 @@
</vn-icon-button>
</a>
</vn-td>
<vn-td center shrink ng-if="!balance.isInvoice">
<vn-td center shrink ng-if="balance.isCompensation">
<vn-icon-button
vn-dialog="send_compensation"
icon="outgoing_mail"
@ -144,7 +144,7 @@
vn-acl="salesAssistant"
vn-acl-action="remove"
icon="add"
vn-tooltip="New payment"
vn-tooltip="New payment"
vn-bind="+"
fixed-bottom-right
ng-click="balanceCreate.show()">
@ -155,9 +155,9 @@
company-fk="$ctrl.companyId"
client-fk="$ctrl.$params.id">
</vn-client-balance-create>
<vn-worker-descriptor-popover
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-invoice-out-descriptor-popover
<vn-invoice-out-descriptor-popover
vn-id="invoiceOutDescriptor">
</vn-invoice-out-descriptor-popover>
</vn-invoice-out-descriptor-popover>

View File

@ -55,41 +55,41 @@ class Controller extends Section {
}
})).then(() => this.getBalances());
}
getCurrentBalance() {
const clientRisks = this.$.riskModel.data;
const selectedCompany = this.companyId;
const currentBalance = clientRisks.find(balance => {
return balance.companyFk === selectedCompany;
});
return currentBalance && currentBalance.amount;
}
getBalances() {
const balances = this.$.model.data;
balances.forEach((balance, index) => {
if (index === 0)
balance.balance = this.getCurrentBalance();
balance.balance = this.getCurrentBalance();
if (index > 0) {
let previousBalance = balances[index - 1];
balance.balance = previousBalance.balance - (previousBalance.debit - previousBalance.credit);
}
});
}
showInvoiceOutDescriptor(event, balance) {
if (!balance.isInvoice) return;
if (event.defaultPrevented) return;
this.$.invoiceOutDescriptor.show(event.target, balance.id);
}
changeDescription(balance) {
const params = {description: balance.description};
const endpoint = `Receipts/${balance.id}`;
this.$http.patch(endpoint, params)
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
sendEmail(balance) {

View File

@ -151,5 +151,19 @@ describe('Client', () => {
$httpBackend.flush();
});
});
describe('sendEmail()', () => {
it('should send an email', () => {
jest.spyOn(controller.vnEmail, 'send');
const $data = {id: 1103};
controller.sendEmail($data);
const expectedPath = `Receipts/${$data.id}/balance-compensation-email`;
expect(controller.vnEmail.send).toHaveBeenCalledWith(expectedPath);
});
});
});
});

View File

@ -1 +1,3 @@
BILL: N/INV {{ref}}
BILL: N/INV {{ref}}
Notify compensation: Do you want to report compensation to the client by mail?
Send compensation: Send compensation

View File

@ -10,8 +10,6 @@ export default class Controller extends Section {
onSubmit() {
return this.$.watcher.submit().then(() => {
const query = `Clients/updatePortfolio`;
this.$http.get(query);
this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`);
});
}

View File

@ -1,112 +1,52 @@
<mg-ajax path="Clients/{{patch.params.id}}/updateFiscalData" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.client"
id-field="id"
form="form"
save="patch">
<vn-watcher vn-id="watcher" data="$ctrl.client" id-field="id" form="form" save="patch">
</vn-watcher>
<vn-crud-model
auto-load="true"
url="Provinces/location"
data="provincesLocation"
order="name">
<vn-crud-model auto-load="true" url="Provinces/location" data="provincesLocation" order="name">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Countries"
data="countries"
order="country">
<vn-crud-model auto-load="true" url="Countries" data="countries" order="country">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTaxTypes"
data="sageTaxTypes"
order="vat">
<vn-crud-model auto-load="true" url="SageTaxTypes" data="sageTaxTypes" order="vat">
</vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
vn-two
vn-focus
label="Social name"
ng-model="$ctrl.client.socialName"
rule
info="Only letters, numbers and spaces can be used"
required="true">
<vn-textfield vn-two vn-focus label="Social name" ng-model="$ctrl.client.socialName" rule
info="Only letters, numbers and spaces can be used" required="true">
</vn-textfield>
<vn-textfield
vn-one
label="Tax number"
ng-model="$ctrl.client.fi"
rule>
<vn-textfield vn-one label="Tax number" ng-model="$ctrl.client.fi" rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-two
label="Street"
ng-model="$ctrl.client.street"
rule>
<vn-textfield vn-two label="Street" ng-model="$ctrl.client.street" rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTaxTypeFk"
data="sageTaxTypes"
show-field="vat"
value-field="id"
label="Sage tax type"
vn-acl="salesAssistant"
rule>
<vn-autocomplete vn-one ng-model="$ctrl.client.sageTaxTypeFk" data="sageTaxTypes" show-field="vat"
value-field="id" label="Sage tax type" vn-acl="salesAssistant" rule>
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTransactionTypeFk"
url="SageTransactionTypes"
show-field="transaction"
value-field="id"
label="Sage transaction type"
<vn-autocomplete vn-one ng-model="$ctrl.client.sageTransactionTypeFk" url="SageTransactionTypes"
show-field="transaction" value-field="id" label="Sage transaction type"
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
vn-acl="salesAssistant"
order="transaction"
rule>
vn-acl="salesAssistant" order="transaction" rule>
<tpl-item>{{id}}: {{transaction}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Postcode"
ng-model="$ctrl.client.postcode"
selection="$ctrl.postcode"
url="Postcodes/location"
fields="['code','townFk']"
order="code, townFk"
value-field="code"
show-field="code"
rule>
<vn-datalist vn-one label="Postcode" ng-model="$ctrl.client.postcode" selection="$ctrl.postcode"
url="Postcodes/location" fields="['code','townFk']" order="code, townFk" value-field="code"
show-field="code" rule>
<tpl-item>
{{code}} - {{town.name}} ({{town.province.name}},
{{code}} - {{town.name}} ({{town.province.name}},
{{town.province.country.country}})
</tpl-item>
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New postcode"
ng-click="postcode.open()"
vn-acl="deliveryBoss"
vn-acl-action="remove">
<vn-icon-button icon="add_circle" vn-tooltip="New postcode" ng-click="postcode.open()"
vn-acl="deliveryBoss" vn-acl-action="remove">
</vn-icon-button>
</append>
</vn-datalist>
<vn-datalist vn-id="town" vn-one
label="City"
ng-model="$ctrl.client.city"
selection="$ctrl.town"
url="Towns/location"
fields="['id', 'name', 'provinceFk']"
show-field="name"
value-field="name">
<vn-datalist vn-id="town" vn-one label="City" ng-model="$ctrl.client.city" selection="$ctrl.town"
url="Towns/location" fields="['id', 'name', 'provinceFk']" show-field="name" value-field="name">
<tpl-item>
{{name}}, {{province.name}}
({{province.country.country}})
@ -114,112 +54,64 @@
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.client.provinceFk"
selection="$ctrl.province"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
value-field="id"
rule>
<vn-autocomplete vn-id="province" vn-one label="Province" ng-model="$ctrl.client.provinceFk"
selection="$ctrl.province" data="provincesLocation" fields="['id', 'name', 'countryFk']"
show-field="name" value-field="id" rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-id="country" vn-one
ng-model="$ctrl.client.countryFk"
data="countries"
show-field="country"
value-field="id"
label="Country"
rule>
<vn-autocomplete vn-id="country" vn-one ng-model="$ctrl.client.countryFk" data="countries"
show-field="country" value-field="id" label="Country" rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Active"
ng-model="$ctrl.client.isActive">
<vn-check vn-one label="Active" ng-model="$ctrl.client.isActive">
</vn-check>
<vn-check
vn-one
label="Frozen"
ng-model="$ctrl.client.isFreezed">
<vn-check vn-one label="Frozen" ng-model="$ctrl.client.isFreezed">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Has to invoice"
ng-model="$ctrl.client.hasToInvoice">
<vn-check vn-one label="Has to invoice" ng-model="$ctrl.client.hasToInvoice">
</vn-check>
<vn-check
vn-one
label="Vies"
ng-model="$ctrl.client.isVies">
<vn-check vn-one label="Vies" ng-model="$ctrl.client.isVies">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Notify by email"
ng-model="$ctrl.client.isToBeMailed">
<vn-check vn-one label="Notify by email" ng-model="$ctrl.client.isToBeMailed">
</vn-check>
<vn-check
vn-one
label="Invoice by address"
ng-model="$ctrl.client.hasToInvoiceByAddress">
<vn-check vn-one label="Invoice by address" ng-model="$ctrl.client.hasToInvoiceByAddress">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Is equalizated"
ng-model="$ctrl.client.isEqualizated"
<vn-check vn-one label="Is equalizated" ng-model="$ctrl.client.isEqualizated"
info="In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not."
on-change="$ctrl.onChangeEqualizated(value)">
</vn-check>
<vn-check
vn-one
label="Verified data"
ng-model="$ctrl.client.isTaxDataChecked"
vn-acl="salesAssistant">
<vn-check vn-one label="Verified data" ng-model="$ctrl.client.isTaxDataChecked" vn-acl="salesAssistant">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Incoterms authorization"
ng-model="$ctrl.client.hasIncoterms"
<vn-check vn-one label="Incoterms authorization" ng-model="$ctrl.client.hasIncoterms"
vn-acl="administrative">
</vn-check>
<vn-check vn-one label="Electronic invoice" ng-model="$ctrl.client.hasElectronicInvoice"
vn-acl="administrative">
</vn-check>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
<vn-submit disabled="!watcher.dataChanged()" label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
<vn-button class="cancel" label="Undo changes" disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>
<vn-confirm
vn-id="propagate-isEqualizated"
question="You changed the equalization tax"
message="Do you want to spread the change?"
on-accept="$ctrl.onAcceptEt()">
<vn-confirm vn-id="propagate-isEqualizated" question="You changed the equalization tax"
message="Do you want to spread the change?" on-accept="$ctrl.onAcceptEt()">
</vn-confirm>
<vn-confirm
vn-id="confirm-duplicatedClient"
message="Found a client with this data"
<vn-confirm vn-id="confirm-duplicatedClient" message="Found a client with this data"
on-accept="$ctrl.onAcceptDuplication()">
</vn-confirm>
<!-- New postcode dialog -->
<vn-geo-postcode
vn-id="postcode"
on-response="$ctrl.onResponse($response)">
<vn-geo-postcode vn-id="postcode" on-response="$ctrl.onResponse($response)">
</vn-geo-postcode>

View File

@ -10,4 +10,5 @@ Sage tax type: Tipo de impuesto Sage
Sage transaction type: Tipo de transacción Sage
Previous client: Cliente anterior
In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente
Incoterms authorization: Autorización incoterms
Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica

View File

@ -29,6 +29,7 @@
<vn-thead>
<vn-tr>
<vn-th field="shipped" default-order="DESC" expand>Date</vn-th>
<vn-th field="userFk">Created by</vn-thfield></vn-th>
<vn-th field="description">Comment</vn-th>
<vn-th field="greugeTypeFk">Type</vn-th>
<vn-th field="amount" number>Amount</vn-th>
@ -37,6 +38,8 @@
<vn-tbody>
<vn-tr ng-repeat="greuge in greuges">
<vn-td shrink-datetime>{{::greuge.shipped | date:'dd/MM/yyyy HH:mm' }}</vn-td>
<vn-td><span ng-click="workerDescriptor.show($event, greuge.user.id)"
class="link">{{::greuge.user.name}}</span></vn-td>
<vn-td>
<span title="{{::greuge.description}}">{{::greuge.description}}</span>
</vn-td>
@ -57,3 +60,4 @@
vn-bind="+"
fixed-bottom-right>
</vn-float-button>
<vn-worker-descriptor-popover vn-id="workerDescriptor"></vn-worker-descriptor-popover>

View File

@ -8,6 +8,12 @@ class Controller extends Section {
include: [
{
relation: 'greugeType',
scope: {
fields: ['id', 'name']
},
},
{
relation: 'user',
scope: {
fields: ['id', 'name']
}

View File

@ -1,4 +1,5 @@
Date: Fecha
Comment: Comentario
Amount: Importe
Type: Tipo
Type: Tipo
Created by: Creado por

View File

@ -5,6 +5,24 @@
form="form"
save="patch">
</vn-watcher>
<vn-crud-model
auto-load="true"
url="Companies"
data="companies"
order="code">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses"
order="name">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="DmsTypes"
data="dmsTypes"
order="name">
</vn-crud-model>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
@ -31,14 +49,14 @@
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Expedition date"
vn-one
label="Expedition date"
ng-model="$ctrl.invoiceIn.issued"
vn-focus
rule>
</vn-date-picker>
<vn-date-picker
vn-one
vn-one
label="Operation date"
ng-model="$ctrl.invoiceIn.operated"
rule>
@ -57,17 +75,47 @@
{{id}} - {{name}}
</tpl-item>
</vn-datalist>
<vn-textfield
label="Document"
ng-model="$ctrl.invoiceIn.dmsFk"
ng-change="$ctrl.checkFileExists($ctrl.invoiceIn.dmsFk)"
rule>
<prepend>
<vn-icon-button
disabled="$ctrl.editDownloadDisabled"
ng-if="$ctrl.invoiceIn.dmsFk"
title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile($ctrl.invoiceIn.dmsFk)">
</vn-icon-button>
</prepend>
<append>
<vn-icon-button
disabled="$ctrl.editDownloadDisabled"
ng-if="$ctrl.invoiceIn.dmsFk"
ng-click="$ctrl.openEditDialog($ctrl.invoiceIn.dmsFk)"
icon="edit"
title="{{'Edit document' | translate}}">
</vn-icon-button>
<vn-icon-button
ng-if="!$ctrl.invoiceIn.dmsFk"
ng-click="$ctrl.openCreateDialog()"
icon="add_circle"
title="{{'Create document' | translate}}">
</vn-icon-button>
</append>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Entry date"
vn-one
label="Entry date"
ng-model="$ctrl.invoiceIn.bookEntried"
rule>
</vn-date-picker>
<vn-date-picker
vn-one
label="Accounted date"
vn-one
label="Accounted date"
ng-model="$ctrl.invoiceIn.booked"
rule>
</vn-date-picker>
@ -104,4 +152,164 @@
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>
</form>
<!-- Create edit dms dialog -->
<vn-dialog
vn-id="dmsEditDialog"
message="Edit document"
on-accept="$ctrl.onEdit()">
<tpl-body>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete vn-one required="true"
label="Company"
ng-model="$ctrl.dms.companyId"
url="Companies"
show-field="code"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one required="true"
label="Warehouse"
ng-model="$ctrl.dms.warehouseId"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one required="true"
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
url="DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
required="true"
label="Description"
ng-model="$ctrl.dms.description"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file
vn-one
label="File"
ng-model="$ctrl.dms.files"
on-change="$ctrl.onFileChange($files)"
accept="{{$ctrl.allowedContentTypes}}"
required="false"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check disabled="true"
label="Generate identifier for original file"
ng-model="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
<!-- Create new dms dialog -->
<vn-dialog
vn-id="dmsCreateDialog"
message="Create document"
on-accept="$ctrl.onCreate()">
<tpl-body>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete
vn-one
label="Company"
ng-model="$ctrl.dms.companyId"
data="companies"
show-field="code"
value-field="id"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="$ctrl.dms.warehouseId"
data="warehouses"
show-field="name"
value-field="id"
required="true">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
data="dmsTypes"
show-field="name"
value-field="id"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Description"
ng-model="$ctrl.dms.description"
required="true"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file
vn-one
label="File"
ng-model="$ctrl.dms.files"
on-change="$ctrl.onFileChange($files)"
accept="{{$ctrl.allowedContentTypes}}"
required="true"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check
label="Generate identifier for original file"
ng-model="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Create</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,9 +1,181 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
class Controller extends Section {
constructor($element, $, vnFile) {
super($element, $, vnFile);
this.dms = {
files: [],
hasFile: false,
hasFileAttached: false
};
this.vnFile = vnFile;
this.getAllowedContentTypes();
this._editDownloadDisabled = false;
}
get contentTypesInfo() {
return this.$t('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
get editDownloadDisabled() {
return this._editDownloadDisabled;
}
async checkFileExists(dmsId) {
if (!dmsId) return;
let filter = {
fields: ['id']
};
await this.$http.get(`Dms/${dmsId}`, {filter})
.then(() => this._editDownloadDisabled = false)
.catch(() => this._editDownloadDisabled = true);
}
async getFile(dmsId) {
const path = `Dms/${dmsId}`;
await this.$http.get(path).then(res => {
const dms = res.data && res.data;
this.dms = {
dmsId: dms.id,
reference: dms.reference,
warehouseId: dms.warehouseFk,
companyId: dms.companyFk,
dmsTypeId: dms.dmsTypeFk,
description: dms.description,
hasFile: dms.hasFile,
hasFileAttached: false,
files: []
};
});
}
getAllowedContentTypes() {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
if (res.data.length > 0) {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
}
});
}
openEditDialog(dmsId) {
this.getFile(dmsId).then(() => this.$.dmsEditDialog.show());
}
openCreateDialog() {
this.dms = {
reference: null,
warehouseId: null,
companyId: null,
dmsTypeId: null,
description: null,
hasFile: true,
hasFileAttached: true,
files: null
};
this.$.dmsCreateDialog.show();
}
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
}
onFileChange(files) {
let hasFileAttached = false;
if (files.length > 0)
hasFileAttached = true;
this.$.$applyAsync(() => {
this.dms.hasFileAttached = hasFileAttached;
});
}
onEdit() {
if (!this.dms.companyId)
throw new UserError(`The company can't be empty`);
if (!this.dms.warehouseId)
throw new UserError(`The warehouse can't be empty`);
if (!this.dms.dmsTypeId)
throw new UserError(`The DMS Type can't be empty`);
if (!this.dms.description)
throw new UserError(`The description can't be empty`);
const query = `dms/${this.dms.dmsId}/updateFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$t('Data saved!'));
if (res.data.length > 0) this.invoiceIn.dmsFk = res.data[0].id;
}
});
}
onCreate() {
if (!this.dms.companyId)
throw new UserError(`The company can't be empty`);
if (!this.dms.warehouseId)
throw new UserError(`The warehouse can't be empty`);
if (!this.dms.dmsTypeId)
throw new UserError(`The DMS Type can't be empty`);
if (!this.dms.description)
throw new UserError(`The description can't be empty`);
if (!this.dms.files)
throw new UserError(`The files can't be empty`);
const query = `Dms/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$t('Data saved!'));
if (res.data.length > 0) this.invoiceIn.dmsFk = res.data[0].id;
}
});
}
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.vnComponent('vnInvoiceInBasicData', {
template: require('./index.html'),
controller: Section,
controller: Controller,
bindings: {
invoiceIn: '<'
}

View File

@ -0,0 +1,102 @@
import './index.js';
import watcher from 'core/mocks/watcher';
describe('InvoiceIn', () => {
describe('Component vnInvoiceInBasicData', () => {
let controller;
let $scope;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('invoiceIn'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
const $element = angular.element('<vn-invoice-in-basic-data></vn-invoice-in-basic-data>');
controller = $componentController('vnInvoiceInBasicData', {$element, $scope});
controller.$.watcher = watcher;
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond({});
}));
describe('onFileChange()', () => {
it('should set dms hasFileAttached property to true if has any files', () => {
const files = [{id: 1, name: 'MyFile'}];
controller.onFileChange(files);
$scope.$apply();
expect(controller.dms.hasFileAttached).toBeTruthy();
});
});
describe('checkFileExists()', () => {
it(`should return false if a file exists`, () => {
const fileIdExists = 1;
controller.checkFileExists(fileIdExists);
expect(controller.editDownloadDisabled).toBe(false);
});
});
describe('onEdit()', () => {
it(`should perform a POST query to edit the dms properties`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const dms = {
dmsId: 1,
reference: 'Ref1',
warehouseId: 1,
companyId: 442,
dmsTypeId: 20,
description: 'This is a description',
files: []
};
controller.dms = dms;
const serializedParams = $httpParamSerializer(controller.dms);
const query = `dms/${controller.dms.dmsId}/updateFile?${serializedParams}`;
$httpBackend.expectPOST(query).respond({});
controller.onEdit();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('onCreate()', () => {
it(`should perform a POST query to create a new dms`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const dms = {
reference: 'Ref1',
warehouseId: 1,
companyId: 442,
dmsTypeId: 20,
description: 'This is a description',
files: [{
lastModified: 1668673957761,
lastModifiedDate: new Date(),
name: 'file-example.png',
size: 19653,
type: 'image/png',
webkitRelativePath: ''
}]
};
controller.dms = dms;
const serializedParams = $httpParamSerializer(controller.dms);
const query = `Dms/uploadFile?${serializedParams}`;
$httpBackend.expectPOST(query).respond({});
controller.onCreate();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1 @@
ContentTypesInfo: Allowed file types {{allowedContentTypes}}

View File

@ -0,0 +1,15 @@
Upload file: Subir fichero
Edit file: Editar fichero
Upload: Subir
Document: Documento
ContentTypesInfo: "Tipos de archivo permitidos: {{allowedContentTypes}}"
Generate identifier for original file: Generar identificador para archivo original
File management: Gestión documental
Hard copy: Copia
This file will be deleted: Este fichero va a ser borrado
Are you sure?: Estas seguro?
File deleted: Fichero eliminado
Remove file: Eliminar fichero
Download file: Descargar fichero
Edit document: Editar documento
Create document: Crear documento

View File

@ -26,11 +26,13 @@
Clone Invoice
</vn-item>
<vn-item
ng-if="false"
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
</vn-item>
<vn-item
ng-if="false"
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF

View File

@ -21,3 +21,4 @@ Total net: Total neto
Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF
New InvoiceIn: Nueva Factura

View File

@ -64,7 +64,7 @@ describe('InvoiceOut filter()', () => {
const invoiceOut = await models.InvoiceOut.findById(1, null, options);
await invoiceOut.updateAttribute('hasPdf', true, options);
const result = await models.InvoiceOut.filter(ctx, {}, options);
const result = await models.InvoiceOut.filter(ctx, {id: invoiceOut.id}, options);
expect(result.length).toEqual(1);

View File

@ -11,7 +11,7 @@ describe('SalesMonitor salesFilter()', () => {
const filter = {order: 'id DESC'};
const result = await models.SalesMonitor.salesFilter(ctx, filter, options);
expect(result.length).toEqual(27);
expect(result.length).toBeGreaterThan(25);
await tx.rollback();
} catch (e) {
@ -39,7 +39,7 @@ describe('SalesMonitor salesFilter()', () => {
const filter = {};
const result = await models.SalesMonitor.salesFilter(ctx, filter, options);
expect(result.length).toEqual(16);
expect(result.length).toBeGreaterThan(15);
await tx.rollback();
} catch (e) {
@ -87,7 +87,7 @@ describe('SalesMonitor salesFilter()', () => {
const filter = {};
const result = await models.SalesMonitor.salesFilter(ctx, filter, options);
expect(result.length).toEqual(27);
expect(result.length).toBeGreaterThan(20);
await tx.rollback();
} catch (e) {
@ -130,7 +130,7 @@ describe('SalesMonitor salesFilter()', () => {
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
expect(length).toEqual(10);
expect(length).toBeGreaterThan(10);
expect(anyResult.state).toMatch(/(Libre|Arreglar)/);
await tx.rollback();
@ -171,7 +171,7 @@ describe('SalesMonitor salesFilter()', () => {
const filter = {};
const result = await models.SalesMonitor.salesFilter(ctx, filter, options);
expect(result.length).toEqual(23);
expect(result.length).toBeGreaterThan(20);
await tx.rollback();
} catch (e) {

View File

@ -33,36 +33,36 @@ module.exports = Self => {
const stmt = new ParameterizedSQL(
`SELECT
t.id,
t.packages,
t.warehouseFk,
t.nickname,
t.clientFk,
t.priority,
t.addressFk,
st.code AS ticketStateCode,
st.name AS ticketStateName,
wh.name AS warehouseName,
tob.description AS ticketObservation,
a.street,
a.postalCode,
a.city,
am.name AS agencyModeName,
u.nickname AS userNickname,
vn.ticketTotalVolume(t.id) AS volume,
tob.description
FROM route r
JOIN ticket t ON t.routeFk = r.id
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
LEFT JOIN observationType ot ON tob.observationTypeFk = ot.id
AND ot.code = 'delivery'
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN account.user u ON u.id = r.workerFk
LEFT JOIN vehicle v ON v.id = r.vehicleFk`
t.id,
t.packages,
t.warehouseFk,
t.nickname,
t.clientFk,
t.priority,
t.addressFk,
st.code AS ticketStateCode,
st.name AS ticketStateName,
wh.name AS warehouseName,
tob.description AS ticketObservation,
a.street,
a.postalCode,
a.city,
am.name AS agencyModeName,
u.nickname AS userNickname,
vn.ticketTotalVolume(t.id) AS volume,
tob.description
FROM vn.route r
JOIN ticket t ON t.routeFk = r.id
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
LEFT JOIN observationType ot ON ot.code = 'delivery'
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
AND tob.observationTypeFk = ot.id
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN account.user u ON u.id = r.workerFk
LEFT JOIN vehicle v ON v.id = r.vehicleFk`
);
if (!filter.where) filter.where = {};

View File

@ -20,6 +20,13 @@
translate>
Update volume
</vn-item>
<vn-item
ng-click="$ctrl.deleteCurrentRoute()"
vn-acl="deliveryBoss"
vn-acl-action="remove"
translate>
Delete route
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">

View File

@ -34,6 +34,14 @@ class Controller extends Descriptor {
});
}
deleteCurrentRoute() {
this.$http.delete(`Routes/${this.id}`)
.then(() => {
this.vnApp.showSuccess(this.$t('Route deleted'));
this.$state.go('route.index');
});
}
loadData() {
const filter = {
fields: [

View File

@ -23,4 +23,20 @@ describe('vnRouteDescriptorPopover', () => {
expect(controller.route).toEqual(response);
});
});
describe('deleteCurrentRoute()', () => {
it(`should perform a delete query to delete the current route`, () => {
const id = 1;
jest.spyOn(controller.vnApp, 'showSuccess');
controller._id = id;
$httpBackend.expectDELETE(`Routes/${id}`).respond(200);
controller.deleteCurrentRoute();
$httpBackend.flush();
expect(controller.route).toBeUndefined();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Route deleted');
});
});
});

View File

@ -4,4 +4,6 @@ Send route report: Enviar informe de ruta
Show route report: Ver informe de ruta
Update volume: Actualizar volumen
Volume updated: Volumen actualizado
Delete route: Borrar ruta
Route deleted: Ruta borrada
Are you sure you want to update the volume?: Estas seguro que quieres actualizar el volumen?

View File

@ -0,0 +1,11 @@
concept: concept
quantity: quantity
price: price
discount: discount
reserved: reserved
isPicked: is picked
created: created
originalQuantity: original quantity
itemFk: item
ticketFk: ticket
saleFk: sale

View File

@ -0,0 +1,11 @@
concept: concepto
quantity: cantidad
price: precio
discount: descuento
reserved: reservado
isPicked: esta seleccionado
created: creado
originalQuantity: cantidad original
itemFk: artículo
ticketFk: ticket
saleFk: línea

View File

@ -0,0 +1,22 @@
shipped: shipped
landed: landed
nickname: nickname
location: location
solution: solution
packages: packages
updated: updated
isDeleted: is deleted
priority: priority
zoneFk: zone
zonePrice: zone price
zoneBonus: zone bonus
totalWithVat: total with vat
totalWithoutVat: total without vat
clientFk: client
warehouseFk: warehouse
refFk: reference
addressFk: address
routeFk: route
companyFk: company
agencyModeFk: agency
ticketFk: ticket

View File

@ -0,0 +1,22 @@
shipped: fecha salida
landed: fecha entrega
nickname: alias
location: ubicación
solution: solución
packages: embalajes
updated: fecha última actualización
isDeleted: esta eliminado
priority: prioridad
zoneFk: zona
zonePrice: precio zona
zoneBonus: bonus zona
totalWithVat: total con IVA
totalWithoutVat: total sin IVA
clientFk: cliente
warehouseFk: almacén
refFk: referencia
addressFk: dirección
routeFk: ruta
companyFk: empresa
agencyModeFk: agencia
ticketFk: ticket

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