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

This commit is contained in:
Alex Moreno 2022-06-15 07:15:34 +00:00
commit 8474fb5692
35 changed files with 603 additions and 297 deletions

View File

@ -1,7 +1,6 @@
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('send', {
description: 'Send a RocketChat message',
description: 'Creates a direct message in the chat model for a user or a channel',
accessType: 'WRITE',
accepts: [{
arg: 'to',
@ -31,39 +30,19 @@ module.exports = Self => {
const recipient = to.replace('@', '');
if (sender.name != recipient) {
await sendMessage(sender, to, message);
await models.Chat.create({
senderFk: sender.id,
recipient: to,
dated: new Date(),
checkUserStatus: 0,
message: message,
status: 0,
attempts: 0
});
return true;
}
return false;
};
async function sendMessage(sender, channel, message) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({
statusCode: 200,
message: 'Fake notification sent'
});
});
}
const login = await Self.getServiceAuth();
const avatar = `${login.host}/avatar/${sender.name}`;
const options = {
headers: {
'X-Auth-Token': login.auth.token,
'X-User-Id': login.auth.userId
},
};
return axios.post(`${login.api}/chat.postMessage`, {
'channel': channel,
'avatar': avatar,
'alias': sender.nickname,
'text': message
}, options);
}
};

View File

@ -1,8 +1,6 @@
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('sendCheckingPresence', {
description: 'Sends a RocketChat message to a connected user or department channel',
description: 'Creates a message in the chat model checking the user status',
accessType: 'WRITE',
accepts: [{
arg: 'workerId',
@ -36,6 +34,7 @@ module.exports = Self => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const sender = await models.Account.findById(userId);
const recipient = await models.Account.findById(recipientId, null, myOptions);
// Prevent sending messages to yourself
@ -44,54 +43,16 @@ module.exports = Self => {
if (!recipient)
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
const {data} = await Self.getUserStatus(recipient.name);
if (data) {
if (data.status === 'offline' || data.status === 'busy') {
// Send message to department room
const workerDepartment = await models.WorkerDepartment.findById(recipientId, {
include: {
relation: 'department'
}
}, myOptions);
const department = workerDepartment && workerDepartment.department();
const channelName = department && department.chatName;
if (channelName)
return Self.send(ctx, `#${channelName}`, `@${recipient.name}${message}`);
else
return Self.send(ctx, `@${recipient.name}`, message);
} else
return Self.send(ctx, `@${recipient.name}`, message);
}
};
/**
* Returns the current user status on Rocketchat
*
* @param {string} username - The recipient user name
* @return {Promise} - The request promise
*/
Self.getUserStatus = async function getUserStatus(username) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({
data: {
status: 'online'
}
await models.Chat.create({
senderFk: sender.id,
recipient: `@${recipient.name}`,
dated: new Date(),
checkUserStatus: 1,
message: message,
status: 0,
attempts: 0
});
});
}
const login = await Self.getServiceAuth();
const options = {
params: {username},
headers: {
'X-Auth-Token': login.auth.token,
'X-User-Id': login.auth.userId
},
};
return axios.get(`${login.api}/users.getStatus`, options);
return true;
};
};

View File

@ -0,0 +1,168 @@
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('sendQueued', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [],
returns: {
type: 'object',
root: true
},
http: {
path: `/sendQueued`,
verb: 'POST'
}
});
Self.sendQueued = async() => {
const models = Self.app.models;
const maxAttempts = 3;
const sentStatus = 1;
const errorStatus = 2;
const chats = await models.Chat.find({
where: {
status: {neq: sentStatus},
attempts: {lt: maxAttempts}
}
});
for (let chat of chats) {
if (chat.checkUserStatus) {
try {
await Self.sendCheckingUserStatus(chat);
await updateChat(chat, sentStatus);
} catch (error) {
await updateChat(chat, errorStatus);
}
} else {
try {
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
await updateChat(chat, sentStatus);
} catch (error) {
await updateChat(chat, errorStatus);
}
}
}
};
/**
* Check user status in Rocket
*
* @param {object} chat - The sender id
* @return {Promise} - The request promise
*/
Self.sendCheckingUserStatus = async function sendCheckingUserStatus(chat) {
const models = Self.app.models;
const recipientName = chat.recipient.slice(1);
const recipient = await models.Account.findOne({
where: {
name: recipientName
}
});
const {data} = await Self.getUserStatus(recipient.name);
if (data) {
if (data.status === 'offline' || data.status === 'busy') {
// Send message to department room
const workerDepartment = await models.WorkerDepartment.findById(recipient.id, {
include: {
relation: 'department'
}
});
const department = workerDepartment && workerDepartment.department();
const channelName = department && department.chatName;
if (channelName)
return Self.sendMessage(chat.senderFk, `#${channelName}`, `@${recipient.name}${message}`);
else
return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message);
} else
return Self.sendMessage(chat.senderFk, `@${recipient.name}`, chat.message);
}
};
/**
* Send a rocket message
*
* @param {object} senderFk - The sender id
* @param {string} recipient - The user (@) or channel (#) to send the message
* @param {string} message - The message to send
* @return {Promise} - The request promise
*/
Self.sendMessage = async function sendMessage(senderFk, recipient, message) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({
statusCode: 200,
message: 'Fake notification sent'
});
});
}
const models = Self.app.models;
const sender = await models.Account.findById(senderFk);
const login = await Self.getServiceAuth();
const avatar = `${login.host}/avatar/${sender.name}`;
const options = {
headers: {
'X-Auth-Token': login.auth.token,
'X-User-Id': login.auth.userId
},
};
return axios.post(`${login.api}/chat.postMessage`, {
'channel': recipient,
'avatar': avatar,
'alias': sender.nickname,
'text': message
}, options);
};
/**
* Update status and attempts of a chat
*
* @param {object} chat - The chat
* @param {string} status - The new status
* @return {Promise} - The request promise
*/
async function updateChat(chat, status) {
return chat.updateAttributes({
status: status,
attempts: ++chat.attempts
});
}
/**
* Returns the current user status on Rocketchat
*
* @param {string} username - The recipient user name
* @return {Promise} - The request promise
*/
Self.getUserStatus = async function getUserStatus(username) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({
data: {
status: 'online'
}
});
});
}
const login = await Self.getServiceAuth();
const options = {
params: {username},
headers: {
'X-Auth-Token': login.auth.token,
'X-User-Id': login.auth.userId
},
};
return axios.get(`${login.api}/users.getStatus`, options);
};
};

View File

@ -1,14 +1,14 @@
const app = require('vn-loopback/server/server');
describe('Chat send()', () => {
it('should return a "Fake notification sent" as response', async() => {
it('should return true as response', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something');
expect(response).toEqual(true);
});
it('should retrun false as response', async() => {
it('should return false as response', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something');

View File

@ -1,58 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('Chat sendCheckingPresence()', () => {
const today = new Date();
today.setHours(6, 0);
const ctx = {req: {accessToken: {userId: 1}}};
const chatModel = models.Chat;
const departmentId = 23;
it('should return true as response', async() => {
const workerId = 1107;
it(`should call to send() method with "@HankPym" as recipient argument`, async() => {
spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'getUserStatus').and.returnValue(
new Promise(resolve => {
return resolve({
data: {
status: 'online'
}
});
})
);
let ctx = {req: {accessToken: {userId: 1}}};
let response = await models.Chat.sendCheckingPresence(ctx, workerId, 'I changed something');
await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something');
expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', 'I changed something');
expect(response).toEqual(true);
});
it(`should call to send() method with "#cooler" as recipient argument`, async() => {
spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'getUserStatus').and.returnValue(
new Promise(resolve => {
return resolve({
data: {
status: 'offline'
}
});
})
);
it('should return false as response', async() => {
const salesPersonId = 18;
const tx = await models.Claim.beginTransaction({});
let ctx = {req: {accessToken: {userId: 18}}};
let response = await models.Chat.sendCheckingPresence(ctx, salesPersonId, 'I changed something');
try {
const options = {transaction: tx};
const department = await models.Department.findById(departmentId, null, options);
await department.updateAttribute('chatName', 'cooler');
await chatModel.sendCheckingPresence(ctx, workerId, 'I changed something');
expect(chatModel.send).toHaveBeenCalledWith(ctx, '#cooler', '@HankPym ➔ I changed something');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(response).toEqual(false);
});
});

View File

@ -0,0 +1,41 @@
const models = require('vn-loopback/server/server').models;
describe('Chat sendCheckingPresence()', () => {
const today = new Date();
today.setHours(6, 0);
const chatModel = models.Chat;
it(`should call to sendCheckingUserStatus()`, async() => {
spyOn(chatModel, 'sendCheckingUserStatus').and.callThrough();
const chat = {
checkUserStatus: 1,
status: 0,
attempts: 0
};
await chatModel.destroyAll();
await chatModel.create(chat);
await chatModel.sendQueued();
expect(chatModel.sendCheckingUserStatus).toHaveBeenCalled();
});
it(`should call to sendMessage() method`, async() => {
spyOn(chatModel, 'sendMessage').and.callThrough();
const chat = {
checkUserStatus: 0,
status: 0,
attempts: 0
};
await chatModel.destroyAll();
await chatModel.create(chat);
await chatModel.sendQueued();
expect(chatModel.sendMessage).toHaveBeenCalled();
});
});

View File

@ -19,31 +19,42 @@ module.exports = Self => {
Self.updateData = async() => {
const models = Self.app.models;
// Get files checksum
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig');
const updatableFiles = [];
for (const file of files) {
const fileChecksum = await getChecksum(file);
if (file.checksum != fileChecksum) {
updatableFiles.push({
name: file.name,
checksum: fileChecksum
});
} else
console.debug(`File already updated, skipping...`);
}
if (updatableFiles.length === 0)
return false;
// Download files
const container = await models.TempContainer.container('edi');
const tempPath = path.join(container.client.root, container.name);
// Temporary file clean
await fs.rmdir(`${tempPath}/*`, {recursive: true});
const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig');
console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`);
const FtpClient = require('ftps');
const ftpClient = new FtpClient({
host: ftpConfig.host,
username: ftpConfig.user,
password: ftpConfig.password,
procotol: 'ftp'
});
const files = await Self.rawSql('SELECT fileName, toTable, file, updated FROM edi.fileConfig');
let remoteFile;
let tempDir;
let tempFile;
for (const file of files) {
try {
const fileName = file.file;
const fileNames = updatableFiles.map(file => file.name);
const tables = await Self.rawSql(`
SELECT fileName, toTable, file
FROM edi.tableConfig
WHERE file IN (?)`, [fileNames]);
for (const table of tables) {
const fileName = table.file;
console.debug(`Downloading file ${fileName}...`);
@ -51,58 +62,143 @@ module.exports = Self => {
tempDir = `${tempPath}/${fileName}`;
tempFile = `${tempPath}/${fileName}.zip`;
// if (fs.existsSync(tempFile))
// await fs.unlink(tempFile);
// if (fs.existsSync(tempDir))
// await fs.rmdir(tempDir, {recursive: true});
await extractFile({
ftpClient: ftpClient,
file: file,
paths: {
remoteFile: remoteFile,
tempDir: tempDir,
tempFile: tempFile
}
});
try {
await fs.readFile(tempFile);
} catch (error) {
if (fs.existsSync(tempFile))
await fs.unlink(tempFile);
console.error(error);
if (error.code === 'ENOENT') {
const downloadOutput = await downloadFile(remoteFile, tempFile);
if (downloadOutput.error)
continue;
}
}
console.debug(`Extracting file ${fileName}...`);
await extractFile(tempFile, tempDir);
console.debug(`Updating table ${table.toTable}...`);
await dumpData(tempDir, table);
}
// Update files checksum
for (const file of updatableFiles) {
await Self.rawSql(`
UPDATE edi.fileConfig
SET checksum = ?
WHERE name = ?`,
[file.checksum, file.name]);
}
// Clean files
try {
await fs.remove(tempPath);
} catch (error) {
if (error.code !== 'ENOENT')
throw e;
}
return true;
};
async function extractFile({ftpClient, file, paths}) {
// Download the zip file
ftpClient.get(paths.remoteFile, paths.tempFile);
let ftpClient;
async function getFtpClient() {
if (!ftpClient) {
const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig');
console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`);
// Execute download command
ftpClient.exec(async(err, response) => {
if (response.error) {
console.debug(`Error downloading file... ${response.error}`);
return;
}
const FtpClient = require('ftps');
const AdmZip = require('adm-zip');
const zip = new AdmZip(paths.tempFile);
const entries = zip.getEntries();
zip.extractAllTo(paths.tempDir, false);
await dumpData({file, entries, paths});
await fs.rmdir(paths.tempDir, {recursive: true});
ftpClient = new FtpClient({
host: ftpConfig.host,
username: ftpConfig.user,
password: ftpConfig.password,
procotol: 'ftp'
});
}
async function dumpData({file, entries, paths}) {
const toTable = file.toTable;
const baseName = file.fileName;
return ftpClient;
}
async function getChecksum(file) {
const ftpClient = await getFtpClient();
console.debug(`Checking checksum for file ${file.name}...`);
ftpClient.cat(`codes/${file.name}.txt`);
const response = await new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (response.error) {
console.debug(`Error downloading checksum file... ${response.error}`);
reject(err);
}
resolve(response);
});
});
if (response && response.data) {
const fileContents = response.data;
const rows = fileContents.split('\n');
const row = rows[4];
const columns = row.split(/\s+/);
let fileChecksum;
if (file.keyValue)
fileChecksum = columns[1];
if (!file.keyValue)
fileChecksum = columns[0];
return fileChecksum;
}
}
async function downloadFile(remoteFile, tempFile) {
const ftpClient = await getFtpClient();
ftpClient.get(remoteFile, tempFile);
return new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (response.error) {
console.debug(`Error downloading file... ${response.error}`);
reject(err);
}
resolve(response);
});
});
}
async function extractFile(tempFile, tempDir) {
const JSZip = require('jszip');
try {
await fs.mkdir(tempDir);
} catch (error) {
if (error.code !== 'EEXIST')
throw e;
}
const fileStream = await fs.readFile(tempFile);
if (fileStream) {
const zip = new JSZip();
const zipContents = await zip.loadAsync(fileStream);
if (!zipContents) return;
const fileNames = Object.keys(zipContents.files);
for (const fileName of fileNames) {
const fileContent = await zip.file(fileName).async('nodebuffer');
const dest = path.join(tempDir, fileName);
await fs.writeFile(dest, fileContent);
}
}
}
async function dumpData(tempDir, table) {
const toTable = table.toTable;
const baseName = table.fileName;
const tx = await Self.beginTransaction({});
@ -112,44 +208,22 @@ module.exports = Self => {
const tableName = `edi.${toTable}`;
await Self.rawSql(`DELETE FROM ??`, [tableName], options);
for (const zipEntry of entries) {
const entryName = zipEntry.entryName;
console.log(`Reading file ${entryName}...`);
const dirFiles = await fs.readdir(tempDir);
const files = dirFiles.filter(file => file.startsWith(baseName));
const startIndex = (entryName.length - 10);
const endIndex = (entryName.length - 4);
const dateString = entryName.substring(startIndex, endIndex);
const lastUpdated = new Date();
for (const file of files) {
console.log(`Dumping data from file ${file}...`);
// Format string date to a date object
let updated = null;
if (file.updated) {
updated = new Date(file.updated);
updated.setHours(0, 0, 0, 0);
}
lastUpdated.setFullYear(`20${dateString.substring(4, 6)}`);
lastUpdated.setMonth(parseInt(dateString.substring(2, 4)) - 1);
lastUpdated.setDate(dateString.substring(0, 2));
lastUpdated.setHours(0, 0, 0, 0);
if (updated && lastUpdated <= updated) {
console.debug(`Table ${toTable} already updated, skipping...`);
continue;
}
console.log('Dumping data...');
const templatePath = path.join(__dirname, `./sql/${toTable}.sql`);
const sqlTemplate = fs.readFileSync(templatePath, 'utf8');
const sqlTemplate = await fs.readFile(templatePath, 'utf8');
const filePath = path.join(tempDir, file);
const rawPath = path.join(paths.tempDir, entryName);
await Self.rawSql(sqlTemplate, [rawPath], options);
await Self.rawSql(sqlTemplate, [filePath], options);
await Self.rawSql(`
UPDATE edi.fileConfig
UPDATE edi.tableConfig
SET updated = ?
WHERE fileName = ?
`, [lastUpdated, baseName], options);
`, [new Date(), baseName], options);
}
tx.commit();

View File

@ -3,4 +3,5 @@ module.exports = Self => {
require('../methods/chat/send')(Self);
require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self);
require('../methods/chat/sendQueued')(Self);
};

View File

@ -1,6 +1,39 @@
{
"name": "Chat",
"base": "VnModel",
"options": {
"mysql": {
"table": "chat"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"senderFk": {
"type": "number"
},
"recipient": {
"type": "string"
},
"dated": {
"type": "date"
},
"checkUserStatus": {
"type": "boolean"
},
"message": {
"type": "string"
},
"status": {
"type": "string"
},
"attempts": {
"type": "number"
}
},
"acls": [{
"property": "validations",
"accessType": "EXECUTE",

View File

@ -0,0 +1,13 @@
CREATE TABLE `vn`.`chat` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`senderFk` int(10) unsigned DEFAULT NULL,
`recipient` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
`dated` date DEFAULT NULL,
`checkUserStatus` tinyint(1) DEFAULT NULL,
`message` varchar(200) COLLATE utf8_unicode_ci DEFAULT NULL,
`status` tinyint(1) DEFAULT NULL,
`attempts` int(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `chat_FK` (`senderFk`),
CONSTRAINT `chat_FK` FOREIGN KEY (`senderFk`) REFERENCES `account`.`user` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

@ -0,0 +1 @@
RENAME TABLE `edi`.`fileConfig` to `edi`.`tableConfig`;

View File

@ -0,0 +1,22 @@
CREATE TABLE `edi`.`fileConfig`
(
name varchar(25) NOT NULL,
checksum text NULL,
keyValue tinyint(1) default true NOT NULL,
constraint fileConfig_pk
primary key (name)
);
create unique index fileConfig_name_uindex
on `edi`.`fileConfig` (name);
INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue)
VALUES ('FEC010104', null, 0);
INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue)
VALUES ('VBN020101', null, 1);
INSERT INTO `edi`.`fileConfig` (name, checksum, keyValue)
VALUES ('florecompc2', null, 1);

View File

@ -0,0 +1,8 @@
ALTER TABLE `vn`.`creditInsurance` ADD creditClassificationFk int(11) NULL;
UPDATE `vn`.`creditInsurance` AS `destiny`
SET `destiny`.`creditClassificationFk`= (SELECT creditClassification FROM `vn`.`creditInsurance` AS `origin` WHERE `origin`.id = `destiny`.id);
ALTER TABLE `vn`.`creditInsurance`
ADD CONSTRAINT `creditInsurance_creditClassificationFk` FOREIGN KEY (`creditClassificationFk`)
REFERENCES `vn`.`creditClassification` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,11 @@
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` TRIGGER `vn`.`creditInsurance_beforeInsert`
BEFORE INSERT ON `creditInsurance`
FOR EACH ROW
BEGIN
IF NEW.creditClassificationFk THEN
SET NEW.creditClassification = NEW.creditClassificationFk;
END IF;
END$$
DELIMITER ;

View File

View File

@ -460,7 +460,7 @@ INSERT INTO `vn`.`creditClassification`(`id`, `client`, `dateStart`, `dateEnd`)
(4, 1104, util.VN_CURDATE(), util.VN_CURDATE()),
(5, 1105, util.VN_CURDATE(), util.VN_CURDATE());
INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `creationDate`, `grade`)
INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassificationFk`, `credit`, `creationDate`, `grade`)
VALUES
(1, 1, 3000, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), NULL),
(2, 2, 6000, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL),
@ -2562,6 +2562,12 @@ INSERT INTO `vn`.`supplierAgencyTerm` (`agencyFk`, `supplierFk`, `minimumPackage
(4, 2, 0, 20.00, 0.00, NULL, 0, 0.00, 0),
(5, 442, 0, 0.00, 3.05, NULL, 0, 0.00, 0);
INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`)
VALUES
(1101, '@PetterParker', CURDATE(), 1, 'First test message', 0, 0),
(1101, '@PetterParker', CURDATE(), 0, 'Second test message', 0, 0);
INSERT INTO `vn`.`mobileAppVersionControl` (`appName`, `version`, `isVersionCritical`)
VALUES
('delivery', '9.2', 0),

View File

@ -28,12 +28,12 @@ describe('Client defaulter path', () => {
const salesPersonName =
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
expect(clientName).toEqual('Ororo Munroe');
expect(salesPersonName).toEqual('salesPersonNick');
expect(clientName).toEqual('Bruce Banner');
expect(salesPersonName).toEqual('developer');
});
it('should first observation not changed', async() => {
const expectedObservation = 'Madness, as you know, is like gravity, all it takes is a little push';
const expectedObservation = 'Meeting with Black Widow 21st 9am';
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(result).toContain(expectedObservation);

View File

@ -15,8 +15,9 @@ export default function currency($translate) {
maximumFractionDigits: fractionSize
};
const lang = $translate.use() == 'es' ? 'de' : $translate.use();
if (typeof input == 'number') {
return new Intl.NumberFormat($translate.use(), options)
return new Intl.NumberFormat(lang, options)
.format(input);
}

View File

@ -85,7 +85,6 @@
}
.icon-bucket:before {
content: "\e97a";
color: #000;
}
.icon-buscaman:before {
content: "\e93b";
@ -95,32 +94,26 @@
}
.icon-calc_volum .path1:before {
content: "\e915";
color: rgb(0, 0, 0);
}
.icon-calc_volum .path2:before {
content: "\e916";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path3:before {
content: "\e917";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path4:before {
content: "\e918";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path5:before {
content: "\e919";
margin-left: -1em;
color: rgb(0, 0, 0);
}
.icon-calc_volum .path6:before {
content: "\e91a";
margin-left: -1em;
color: rgb(255, 255, 255);
}
.icon-calendar:before {
content: "\e93d";

View File

@ -1,21 +1,39 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client updatePortfolio', () => {
const clientId = 1108;
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 clientQuery = `UPDATE vn.client SET salesPersonFk = ${salesPersonId} WHERE id = ${clientId}; `;
await models.Client.rawSql(clientQuery);
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('salesPersonFk', salesPersonId, options);
await models.Client.updatePortfolio();
await models.Client.updatePortfolio(options);
const portfolioQuery = `SELECT portfolioWeight FROM bs.salesPerson WHERE workerFk = ${salesPersonId}; `;
const [salesPerson] = await models.Client.rawSql(portfolioQuery, null, options);
@ -30,21 +48,21 @@ describe('Client updatePortfolio', () => {
});
it('should keep the same portfolioWeight when a salesperson is unassigned of a client', async() => {
pending('task 3817');
const clientId = 1107;
const salesPersonId = 19;
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const expectedResult = 34.40;
await models.Client.rawSql(`UPDATE vn.client SET salesPersonFk = NULL WHERE id = ${clientId}; `);
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, null, options);
const [salesPerson] = await models.Client.rawSql(portfolioQuery);
expect(salesPerson.portfolioWeight).toEqual(expectedResult);

View File

@ -13,8 +13,13 @@ module.exports = function(Self) {
}
});
Self.updatePortfolio = async() => {
Self.updatePortfolio = async options => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
query = `CALL bs.salesPerson_updatePortfolio()`;
return await Self.rawSql(query);
return Self.rawSql(query, null, myOptions);
};
};

View File

@ -38,7 +38,7 @@ module.exports = Self => {
}, myOptions);
await models.CreditInsurance.create({
creditClassification: newClassification.id,
creditClassificationFk: newClassification.id,
credit: data.credit,
grade: data.grade
}, myOptions);

View File

@ -60,12 +60,12 @@ module.exports = Self => {
DISTINCT c.id clientFk,
c.name clientName,
c.salesPersonFk,
u.nickname salesPersonName,
u.name salesPersonName,
d.amount,
co.created,
co.text observation,
uw.id workerFk,
uw.nickname workerName,
uw.name workerName,
c.creditInsurance,
d.defaulterSinced
FROM vn.defaulter d

View File

@ -136,6 +136,9 @@
"mysql": {
"columnName": "businessTypeFk"
}
},
"salesPersonFk": {
"type": "number"
}
},
"relations": {

View File

@ -36,7 +36,7 @@
"insurances": {
"type": "hasMany",
"model": "CreditInsurance",
"foreignKey": "creditClassification"
"foreignKey": "creditClassificationFk"
}
}
}

View File

@ -24,7 +24,7 @@ module.exports = function(Self) {
let filter = {
fields: ['grade'],
where: {
creditClassification: this.creditClassification
creditClassificationFk: this.creditClassificationFk
},
order: 'created DESC'
};

View File

@ -30,7 +30,7 @@
"classification": {
"type": "belongsTo",
"model": "CreditClassification",
"foreignKey": "creditClassification"
"foreignKey": "creditClassificationFk"
}
},
"scope": {

View File

@ -54,14 +54,7 @@
show-field="bic"
vn-acl="salesAssistant"
disabled="$ctrl.ibanCountry == 'ES'">
<tpl-item>
<vn-horizontal>
<vn-one>{{bic}}</vn-one>
<vn-one>
<div class="ellipsize" style="max-width: 10em">{{name}}</div>
</vn-one>
</vn-horizontal>
</tpl-item>
<tpl-item>{{bic}} {{name}}</tpl-item>
<append>
<vn-icon-button
vn-auto

View File

@ -1,7 +1,7 @@
<vn-crud-model
vn-id="model"
url="CreditInsurances"
link="{creditClassification: $ctrl.$params.classificationId}"
link="{creditClassificationFk: $ctrl.$params.classificationId}"
limit="20"
data="insurances"
auto-load="true">

View File

@ -3,6 +3,7 @@
url="Defaulters/filter"
filter="::$ctrl.filter"
limit="20"
order="amount DESC"
data="defaulters"
auto-load="true">
</vn-crud-model>
@ -70,13 +71,13 @@
</th>
<th
vn-tooltip="Last observation date"
field="created"
shrink-datetime>
<span translate>Last observation D.</span>
field="created">
<span translate>L. O. Date</span>
</th>
<th
vn-tooltip="Credit insurance"
field="creditInsurance" >
field="creditInsurance"
shrink>
<span translate>Credit I.</span>
</th>
<th field="defaulterSinced">
@ -124,13 +125,13 @@
ng-model="defaulter.observation">
</vn-textarea>
</td>
<td shrink-datetime>
<td shrink-date>
<span class="chip {{::$ctrl.chipColor(defaulter.created)}}">
{{::defaulter.created | date: 'dd/MM/yyyy'}}
</span>
</td>
<td>{{::defaulter.creditInsurance | currency: 'EUR': 2}}</td>
<td>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</td>
<td shrink>{{::defaulter.creditInsurance | currency: 'EUR': 2}}</td>
<td shrink-date>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</td>
</tr>
</tbody>
</table>

View File

@ -26,7 +26,7 @@ export default class Controller extends Section {
url: 'Workers/activeWithInheritedRole',
where: `{role: 'salesPerson'}`,
searchFunction: '{firstName: $search}',
showField: 'nickname',
showField: 'name',
valueField: 'id',
}
},
@ -35,7 +35,7 @@ export default class Controller extends Section {
autocomplete: {
url: 'Workers/activeWithInheritedRole',
searchFunction: '{firstName: $search}',
showField: 'nickname',
showField: 'name',
valueField: 'id',
}
},
@ -53,16 +53,8 @@ export default class Controller extends Section {
}
]
};
}
get balanceDueTotal() {
let balanceDueTotal = 0;
const defaulters = this.$.model.data || [];
for (let defaulter of defaulters)
balanceDueTotal += defaulter.amount;
return balanceDueTotal;
this.getBalanceDueTotal();
}
get checked() {
@ -76,6 +68,18 @@ export default class Controller extends Section {
return checkedLines;
}
getBalanceDueTotal() {
this.$http.get('Defaulters/filter')
.then(res => {
if (!res.data) return 0;
this.balanceDueTotal = res.data.reduce(
(accumulator, currentValue) => {
return accumulator + (currentValue['amount'] || 0);
}, 0);
});
}
chipColor(date) {
const day = 24 * 60 * 60 * 1000;
const today = new Date();

View File

@ -36,17 +36,6 @@ describe('client defaulter', () => {
});
});
describe('balanceDueTotal() getter', () => {
it('should return balance due total', () => {
const data = controller.$.model.data;
const expectedAmount = data[0].amount + data[1].amount + data[2].amount;
const result = controller.balanceDueTotal;
expect(result).toEqual(expectedAmount);
});
});
describe('chipColor()', () => {
it('should return undefined when the date is the present', () => {
let today = new Date();
@ -93,6 +82,7 @@ describe('client defaulter', () => {
const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}];
jest.spyOn(controller.vnApp, 'showMessage');
$httpBackend.expect('GET', `Defaulters/filter`).respond(200);
$httpBackend.expect('POST', `ClientObservations`, params).respond(200, params);
controller.onResponse();
@ -115,5 +105,17 @@ describe('client defaulter', () => {
expect(expr).toEqual({'d.clientFk': '5'});
});
});
describe('getBalanceDueTotal()', () => {
it('should return balance due total', () => {
const defaulters = controller.$.model.data;
$httpBackend.when('GET', `Defaulters/filter`).respond(defaulters);
controller.getBalanceDueTotal();
$httpBackend.flush();
expect(controller.balanceDueTotal).toEqual(875);
});
});
});
});

View File

@ -3,7 +3,7 @@ Add observation to all selected clients: Añadir observación a {{total}} client
Balance D.: Saldo V.
Credit I.: Crédito A.
Last observation: Última observación
Last observation D.: Fecha última O.
L. O. Date: Fecha Ú. O.
Last observation date: Fecha última observación
Search client: Buscar clientes
Worker who made the last observation: Trabajador que ha realizado la última observación

View File

@ -25,8 +25,13 @@
label="Bank entity"
ng-model="supplierAccount.bankEntityFk"
url="BankEntities"
show-field="name"
fields="['name']"
initial-data="supplierAccount.bankEntityFk"
search-function="{or: [{bic: {like: $search +'%'}}, {name: {like: '%'+ $search +'%'}}]}"
value-field="id"
show-field="bic"
rule>
<tpl-item>{{bic}} {{name}}</tpl-item>
<append>
<vn-icon-button
vn-auto

View File

@ -12,7 +12,6 @@
"node": ">=14"
},
"dependencies": {
"adm-zip": "^0.5.9",
"axios": "^0.25.0",
"bmp-js": "^0.1.0",
"compression": "^1.7.3",
@ -23,6 +22,7 @@
"i18n": "^0.8.4",
"image-type": "^4.1.0",
"imap": "^0.8.19",
"jszip": "^3.10.0",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
"loopback-boot": "3.3.1",