refs #5423 añadido 3r decimal #1392
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -5,14 +5,22 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2312.01] - 2023-04-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
-
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
-
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
-
|
||||||
|
|
||||||
## [2310.01] - 2023-03-23
|
## [2310.01] - 2023-03-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
|
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
|
||||||
|
|
||||||
### Changed
|
|
||||||
-
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
|
- (Clientes -> Listado extendido) Resuelto error al filtrar por clientes inactivos desde la columna "Activo"
|
||||||
- (General) Al pasar el ratón por encima del icono de "Borrar" en un campo, se hacía más grande afectando a la interfaz
|
- (General) Al pasar el ratón por encima del icono de "Borrar" en un campo, se hacía más grande afectando a la interfaz
|
||||||
|
|
|
@ -81220,3 +81220,4 @@ USE `vn`;
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
-- Dump completed on 2023-02-21 8:14:30
|
-- Dump completed on 2023-02-21 8:14:30
|
||||||
|
|
||||||
|
|
|
@ -524,7 +524,7 @@ export default {
|
||||||
},
|
},
|
||||||
itemLog: {
|
itemLog: {
|
||||||
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
|
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
|
||||||
fifthLineCreatedProperty: 'vn-item-log > vn-log vn-tbody > vn-tr:nth-child(5) table tr:nth-child(2) td.after',
|
fifthLineCreatedProperty: 'vn-item-log > vn-log vn-tbody > vn-tr:nth-child(5) table tr:nth-child(4) td.after',
|
||||||
},
|
},
|
||||||
ticketSummary: {
|
ticketSummary: {
|
||||||
header: 'vn-ticket-summary > vn-card > h5',
|
header: 'vn-ticket-summary > vn-card > h5',
|
||||||
|
|
|
@ -59,6 +59,6 @@ describe('Item log path', () => {
|
||||||
const fifthLineCreatedProperty = await page
|
const fifthLineCreatedProperty = await page
|
||||||
.waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText');
|
.waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText');
|
||||||
|
|
||||||
expect(fifthLineCreatedProperty).toEqual('Coral y materiales similares');
|
expect(fifthLineCreatedProperty).toEqual('05080000');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,6 +37,6 @@ describe('Zone descriptor path', () => {
|
||||||
await page.accessToSection('ticket.card.log');
|
await page.accessToSection('ticket.card.log');
|
||||||
const lastChanges = await page.waitToGetProperty(selectors.ticketLog.changes, 'innerText');
|
const lastChanges = await page.waitToGetProperty(selectors.ticketLog.changes, 'innerText');
|
||||||
|
|
||||||
expect(lastChanges).toContain('Arreglar');
|
expect(lastChanges).toContain('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
const pick = require('object.pick');
|
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
module.exports = function(Self) {
|
module.exports = function(Self) {
|
||||||
|
@ -6,344 +5,11 @@ module.exports = function(Self) {
|
||||||
Self.super_.setup.call(this);
|
Self.super_.setup.call(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
Self.observe('after save', async function(ctx) {
|
|
||||||
const loopBackContext = LoopBackContext.getCurrentContext();
|
|
||||||
await logInModel(ctx, loopBackContext);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self.observe('before save', async function(ctx) {
|
Self.observe('before save', async function(ctx) {
|
||||||
const appModels = ctx.Model.app.models;
|
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
|
||||||
const definition = ctx.Model.definition;
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
// Check for transactions
|
|
||||||
if (ctx.options && ctx.options.transaction)
|
|
||||||
options.transaction = ctx.options.transaction;
|
|
||||||
|
|
||||||
let oldInstance;
|
|
||||||
let newInstance;
|
|
||||||
|
|
||||||
if (ctx.data) {
|
|
||||||
const changes = pick(ctx.currentInstance, Object.keys(ctx.data));
|
|
||||||
newInstance = ctx.data;
|
|
||||||
oldInstance = changes;
|
|
||||||
|
|
||||||
if (ctx.where && !ctx.currentInstance) {
|
|
||||||
const fields = Object.keys(ctx.data);
|
|
||||||
const modelName = definition.name;
|
|
||||||
|
|
||||||
ctx.oldInstances = await appModels[modelName].find({
|
|
||||||
where: ctx.where,
|
|
||||||
fields: fields
|
|
||||||
}, options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get changes from created instance
|
|
||||||
if (ctx.isNewInstance)
|
|
||||||
newInstance = ctx.instance.__data;
|
|
||||||
|
|
||||||
ctx.hookState.oldInstance = oldInstance;
|
|
||||||
ctx.hookState.newInstance = newInstance;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.observe('before delete', async function(ctx) {
|
Self.observe('before delete', async function(ctx) {
|
||||||
const appModels = ctx.Model.app.models;
|
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
|
||||||
const definition = ctx.Model.definition;
|
|
||||||
const relations = ctx.Model.relations;
|
|
||||||
|
|
||||||
let options = {};
|
|
||||||
if (ctx.options && ctx.options.transaction)
|
|
||||||
options.transaction = ctx.options.transaction;
|
|
||||||
|
|
||||||
if (ctx.where) {
|
|
||||||
let affectedModel = definition.name;
|
|
||||||
let deletedInstances = await appModels[affectedModel].find({
|
|
||||||
where: ctx.where
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
let relation = definition.settings.log.relation;
|
|
||||||
|
|
||||||
if (relation) {
|
|
||||||
let primaryKey = relations[relation].keyFrom;
|
|
||||||
|
|
||||||
let arrangedDeletedInstances = [];
|
|
||||||
for (let i = 0; i < deletedInstances.length; i++) {
|
|
||||||
if (primaryKey)
|
|
||||||
deletedInstances[i].originFk = deletedInstances[i][primaryKey];
|
|
||||||
let arrangedInstance = await fkToValue(deletedInstances[i], ctx);
|
|
||||||
arrangedDeletedInstances[i] = arrangedInstance;
|
|
||||||
}
|
|
||||||
ctx.hookState.oldInstance = arrangedDeletedInstances;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.observe('after delete', async function(ctx) {
|
|
||||||
const loopBackContext = LoopBackContext.getCurrentContext();
|
|
||||||
if (ctx.hookState.oldInstance)
|
|
||||||
logDeletedInstances(ctx, loopBackContext);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function logDeletedInstances(ctx, loopBackContext) {
|
|
||||||
const appModels = ctx.Model.app.models;
|
|
||||||
const definition = ctx.Model.definition;
|
|
||||||
let options = {};
|
|
||||||
if (ctx.options && ctx.options.transaction)
|
|
||||||
options.transaction = ctx.options.transaction;
|
|
||||||
|
|
||||||
ctx.hookState.oldInstance.forEach(async instance => {
|
|
||||||
let userFk;
|
|
||||||
if (loopBackContext)
|
|
||||||
userFk = loopBackContext.active.accessToken.userId;
|
|
||||||
|
|
||||||
let changedModelValue = definition.settings.log.changedModelValue;
|
|
||||||
let logRecord = {
|
|
||||||
originFk: instance.originFk,
|
|
||||||
userFk: userFk,
|
|
||||||
action: 'delete',
|
|
||||||
changedModel: definition.name,
|
|
||||||
changedModelId: instance.id,
|
|
||||||
changedModelValue: instance[changedModelValue],
|
|
||||||
oldInstance: instance,
|
|
||||||
newInstance: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
delete instance.originFk;
|
|
||||||
|
|
||||||
let logModel = definition.settings.log.model;
|
|
||||||
await appModels[logModel].create(logRecord, options);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get log values from a foreign key
|
|
||||||
async function fkToValue(instance, ctx) {
|
|
||||||
const appModels = ctx.Model.app.models;
|
|
||||||
const relations = ctx.Model.relations;
|
|
||||||
let options = {};
|
|
||||||
|
|
||||||
// Check for transactions
|
|
||||||
if (ctx.options && ctx.options.transaction)
|
|
||||||
options.transaction = ctx.options.transaction;
|
|
||||||
|
|
||||||
const instanceCopy = JSON.parse(JSON.stringify(instance));
|
|
||||||
const result = {};
|
|
||||||
for (const key in instanceCopy) {
|
|
||||||
let value = instanceCopy[key];
|
|
||||||
|
|
||||||
if (value instanceof Object)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (value === undefined) continue;
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
for (let relationName in relations) {
|
|
||||||
const relation = relations[relationName];
|
|
||||||
if (relation.keyFrom == key && key != 'id') {
|
|
||||||
const model = relation.modelTo;
|
|
||||||
const modelName = relation.modelTo.modelName;
|
|
||||||
const properties = model && model.definition.properties;
|
|
||||||
const settings = model && model.definition.settings;
|
|
||||||
|
|
||||||
const recordSet = await appModels[modelName].findById(value, null, options);
|
|
||||||
|
|
||||||
const hasShowField = settings.log && settings.log.showField;
|
|
||||||
let showField = hasShowField && recordSet
|
|
||||||
&& recordSet[settings.log.showField];
|
|
||||||
|
|
||||||
if (!showField) {
|
|
||||||
const showFieldNames = [
|
|
||||||
'name',
|
|
||||||
'description',
|
|
||||||
'code',
|
|
||||||
'nickname'
|
|
||||||
];
|
|
||||||
for (field of showFieldNames) {
|
|
||||||
const propField = properties && properties[field];
|
|
||||||
const recordField = recordSet && recordSet[field];
|
|
||||||
|
|
||||||
if (propField && recordField) {
|
|
||||||
showField = field;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showField && recordSet && recordSet[showField]) {
|
|
||||||
value = recordSet[showField];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = recordSet && recordSet.id || value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result[key] = value;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logInModel(ctx, loopBackContext) {
|
|
||||||
const appModels = ctx.Model.app.models;
|
|
||||||
const definition = ctx.Model.definition;
|
|
||||||
const defSettings = ctx.Model.definition.settings;
|
|
||||||
const relations = ctx.Model.relations;
|
|
||||||
|
|
||||||
const options = {};
|
|
||||||
if (ctx.options && ctx.options.transaction)
|
|
||||||
options.transaction = ctx.options.transaction;
|
|
||||||
|
|
||||||
let primaryKey;
|
|
||||||
for (let property in definition.properties) {
|
|
||||||
if (definition.properties[property].id) {
|
|
||||||
primaryKey = property;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!primaryKey) throw new Error('Primary key not found');
|
|
||||||
let originId;
|
|
||||||
|
|
||||||
// RELATIONS LOG
|
|
||||||
let changedModelId;
|
|
||||||
|
|
||||||
if (ctx.instance && !defSettings.log.relation) {
|
|
||||||
originId = ctx.instance.id;
|
|
||||||
changedModelId = ctx.instance.id;
|
|
||||||
} else if (defSettings.log.relation) {
|
|
||||||
primaryKey = relations[defSettings.log.relation].keyFrom;
|
|
||||||
|
|
||||||
if (ctx.where && ctx.where[primaryKey])
|
|
||||||
originId = ctx.where[primaryKey];
|
|
||||||
else if (ctx.instance) {
|
|
||||||
originId = ctx.instance[primaryKey];
|
|
||||||
changedModelId = ctx.instance.id;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
originId = ctx.currentInstance.id;
|
|
||||||
changedModelId = ctx.currentInstance.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the changedModelValue to save and the instances changed in case its an updateAll
|
|
||||||
let showField = defSettings.log.showField;
|
|
||||||
let where;
|
|
||||||
if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) {
|
|
||||||
changedModelId = [];
|
|
||||||
where = [];
|
|
||||||
let changedInstances = await appModels[definition.name].find({
|
|
||||||
where: ctx.where,
|
|
||||||
fields: ['id', showField, primaryKey]
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
changedInstances.forEach(element => {
|
|
||||||
where.push(element[showField]);
|
|
||||||
changedModelId.push(element.id);
|
|
||||||
originId = element[primaryKey];
|
|
||||||
});
|
|
||||||
} else if (ctx.hookState.oldInstance)
|
|
||||||
where = ctx.instance[showField];
|
|
||||||
|
|
||||||
// Set oldInstance, newInstance, userFk and action
|
|
||||||
let oldInstance = {};
|
|
||||||
if (ctx.hookState.oldInstance)
|
|
||||||
Object.assign(oldInstance, ctx.hookState.oldInstance);
|
|
||||||
|
|
||||||
let newInstance = {};
|
|
||||||
if (ctx.hookState.newInstance)
|
|
||||||
Object.assign(newInstance, ctx.hookState.newInstance);
|
|
||||||
let userFk;
|
|
||||||
if (loopBackContext)
|
|
||||||
userFk = loopBackContext.active.accessToken.userId;
|
|
||||||
|
|
||||||
let action = setActionType(ctx);
|
|
||||||
|
|
||||||
removeUnloggable(definition, oldInstance);
|
|
||||||
removeUnloggable(definition, newInstance);
|
|
||||||
|
|
||||||
oldInstance = await fkToValue(oldInstance, ctx);
|
|
||||||
newInstance = await fkToValue(newInstance, ctx);
|
|
||||||
|
|
||||||
// Prevent log with no new changes
|
|
||||||
const hasNewChanges = Object.keys(newInstance).length;
|
|
||||||
if (!hasNewChanges) return;
|
|
||||||
|
|
||||||
let logRecord = {
|
|
||||||
originFk: originId,
|
|
||||||
userFk: userFk,
|
|
||||||
action: action,
|
|
||||||
changedModel: definition.name,
|
|
||||||
changedModelId: changedModelId, // Model property with an different data type will throw a NaN error
|
|
||||||
changedModelValue: where,
|
|
||||||
oldInstance: oldInstance,
|
|
||||||
newInstance: newInstance
|
|
||||||
};
|
|
||||||
|
|
||||||
let logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx);
|
|
||||||
let logModel = defSettings.log.model;
|
|
||||||
|
|
||||||
await appModels[logModel].create(logsToSave, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes unwanted properties
|
|
||||||
* @param {*} definition Model definition
|
|
||||||
* @param {*} properties Modified object properties
|
|
||||||
*/
|
|
||||||
function removeUnloggable(definition, properties) {
|
|
||||||
const objectCopy = Object.assign({}, properties);
|
|
||||||
const propList = Object.keys(objectCopy);
|
|
||||||
const propDefs = new Map();
|
|
||||||
|
|
||||||
for (let property in definition.properties) {
|
|
||||||
const propertyDef = definition.properties[property];
|
|
||||||
|
|
||||||
propDefs.set(property, propertyDef);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let property of propList) {
|
|
||||||
const propertyDef = propDefs.get(property);
|
|
||||||
const firstChar = property.substring(0, 1);
|
|
||||||
const isPrivate = firstChar == '$';
|
|
||||||
|
|
||||||
if (isPrivate || !propertyDef)
|
|
||||||
delete properties[property];
|
|
||||||
|
|
||||||
if (!propertyDef) continue;
|
|
||||||
|
|
||||||
if (propertyDef.log === false || isPrivate)
|
|
||||||
delete properties[property];
|
|
||||||
else if (propertyDef.logValue === false)
|
|
||||||
properties[property] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function retuns all the instances changed in case this is an updateAll
|
|
||||||
function setLogsToSave(changedInstances, changedInstancesIds, logRecord, ctx) {
|
|
||||||
let promises = [];
|
|
||||||
if (changedInstances && typeof changedInstances == 'object') {
|
|
||||||
for (let i = 0; i < changedInstances.length; i++) {
|
|
||||||
logRecord.changedModelId = changedInstancesIds[i];
|
|
||||||
logRecord.changedModelValue = changedInstances[i];
|
|
||||||
if (ctx.oldInstances)
|
|
||||||
logRecord.oldInstance = ctx.oldInstances[i];
|
|
||||||
promises.push(JSON.parse(JSON.stringify(logRecord)));
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
return logRecord;
|
|
||||||
|
|
||||||
return promises;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActionType(ctx) {
|
|
||||||
let oldInstance = ctx.hookState.oldInstance;
|
|
||||||
let newInstance = ctx.hookState.newInstance;
|
|
||||||
|
|
||||||
if (oldInstance && newInstance)
|
|
||||||
return 'update';
|
|
||||||
else if (!oldInstance && newInstance)
|
|
||||||
return 'insert';
|
|
||||||
|
|
||||||
return 'delete';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,8 +2,41 @@ const mysql = require('mysql');
|
||||||
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
||||||
const MySQL = require('loopback-connector-mysql').MySQL;
|
const MySQL = require('loopback-connector-mysql').MySQL;
|
||||||
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
|
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
|
||||||
|
const Transaction = require('loopback-connector').Transaction;
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const limitSet = new Set([
|
||||||
|
'save',
|
||||||
|
'updateOrCreate',
|
||||||
|
'replaceOrCreate',
|
||||||
|
'replaceById',
|
||||||
|
'update'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const opOpts = {
|
||||||
|
update: [
|
||||||
|
'update',
|
||||||
|
'replaceById',
|
||||||
|
// |insert
|
||||||
|
'save',
|
||||||
|
'updateOrCreate',
|
||||||
|
'replaceOrCreate'
|
||||||
|
],
|
||||||
|
delete: [
|
||||||
|
'destroy',
|
||||||
|
'destroyAll'
|
||||||
|
],
|
||||||
|
insert: [
|
||||||
|
'create'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const opMap = new Map();
|
||||||
|
for (const op in opOpts) {
|
||||||
|
for (const met of opOpts[op])
|
||||||
|
opMap.set(met, op);
|
||||||
|
}
|
||||||
|
|
||||||
class VnMySQL extends MySQL {
|
class VnMySQL extends MySQL {
|
||||||
/**
|
/**
|
||||||
* Promisified version of execute().
|
* Promisified version of execute().
|
||||||
|
@ -219,6 +252,277 @@ class VnMySQL extends MySQL {
|
||||||
this.makePagination(filter)
|
this.makePagination(filter)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create(model, data, opts, cb) {
|
||||||
|
const ctx = {data};
|
||||||
|
this.invokeMethod('create',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
createAll(model, data, opts, cb) {
|
||||||
|
const ctx = {data};
|
||||||
|
this.invokeMethod('createAll',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
save(model, data, opts, cb) {
|
||||||
|
const ctx = {data};
|
||||||
|
this.invokeMethod('save',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOrCreate(model, data, opts, cb) {
|
||||||
|
const ctx = {data};
|
||||||
|
this.invokeMethod('updateOrCreate',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceOrCreate(model, data, opts, cb) {
|
||||||
|
const ctx = {data};
|
||||||
|
this.invokeMethod('replaceOrCreate',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyAll(model, where, opts, cb) {
|
||||||
|
const ctx = {where};
|
||||||
|
this.invokeMethod('destroyAll',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(model, where, data, opts, cb) {
|
||||||
|
const ctx = {where, data};
|
||||||
|
this.invokeMethod('update',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceById(model, id, data, opts, cb) {
|
||||||
|
const ctx = {id, data};
|
||||||
|
this.invokeMethod('replaceById',
|
||||||
|
arguments, model, ctx, opts, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggable(model) {
|
||||||
|
const Model = this.getModelDefinition(model).model;
|
||||||
|
const settings = Model.definition.settings;
|
||||||
|
return settings.base && settings.base === 'Loggable';
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeMethod(method, args, model, ctx, opts, cb) {
|
||||||
|
if (!this.isLoggable(model))
|
||||||
|
return super[method].apply(this, args);
|
||||||
|
|
||||||
|
this.invokeMethodP(method, [...args], model, ctx, opts)
|
||||||
|
.then(res => cb(...res), cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
async invokeMethodP(method, args, model, ctx, opts) {
|
||||||
|
const Model = this.getModelDefinition(model).model;
|
||||||
|
const settings = Model.definition.settings;
|
||||||
|
let tx;
|
||||||
|
if (!opts.transaction) {
|
||||||
|
tx = await Transaction.begin(this, {});
|
||||||
|
opts = Object.assign({transaction: tx, httpCtx: opts.httpCtx}, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch old values (update|delete) or login
|
||||||
|
let where, id, data, idName, limit, op, oldInstances, newInstances;
|
||||||
|
const hasGrabUser = settings.log && settings.log.grabUser;
|
||||||
|
if(hasGrabUser){
|
||||||
|
const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
|
||||||
|
const user = await Model.app.models.Account.findById(userId, {fields: ['name']}, opts);
|
||||||
|
await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
where = ctx.where;
|
||||||
|
id = ctx.id;
|
||||||
|
data = ctx.data;
|
||||||
|
idName = this.idName(model);
|
||||||
|
|
||||||
|
limit = limitSet.has(method);
|
||||||
|
|
||||||
|
op = opMap.get(method);
|
||||||
|
|
||||||
|
if (!where) {
|
||||||
|
if (id) where = {[idName]: id};
|
||||||
|
else where = {[idName]: data[idName]};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch old values
|
||||||
|
switch (op) {
|
||||||
|
case 'update':
|
||||||
|
case 'delete':
|
||||||
|
// Single entity operation
|
||||||
|
const stmt = this.buildSelectStmt(op, data, idName, model, where, limit);
|
||||||
|
stmt.merge(`FOR UPDATE`);
|
||||||
|
oldInstances = await this.executeStmt(stmt, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await new Promise(resolve => {
|
||||||
|
const fnArgs = args.slice(0, -2);
|
||||||
|
fnArgs.push(opts, (...args) => resolve(args));
|
||||||
|
super[method].apply(this, fnArgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(hasGrabUser)
|
||||||
|
await this.executeP(`CALL account.myUser_logout()`, null, opts);
|
||||||
|
else {
|
||||||
|
// Fetch new values
|
||||||
|
const ids = [];
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 'insert':
|
||||||
|
case 'update': {
|
||||||
|
switch (method) {
|
||||||
|
case 'createAll':
|
||||||
|
for (const row of res[1])
|
||||||
|
ids.push(row[idName]);
|
||||||
|
break;
|
||||||
|
case 'create':
|
||||||
|
ids.push(res[1]);
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
if (data[idName] != null)
|
||||||
|
ids.push(data[idName]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWhere = ids.length ? {[idName]: ids} : where;
|
||||||
|
|
||||||
|
const stmt = this.buildSelectStmt(op, data, idName, model, newWhere, limit);
|
||||||
|
newInstances = await this.executeStmt(stmt, opts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.createLogRecord(oldInstances, newInstances, model, opts);
|
||||||
|
}
|
||||||
|
if (tx) await tx.commit();
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
if (tx) tx.rollback();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSelectStmt(op, data, idName, model, where, limit) {
|
||||||
|
const Model = this.getModelDefinition(model).model;
|
||||||
|
const properties = Object.keys(Model.definition.properties);
|
||||||
|
|
||||||
|
const fields = data ? Object.keys(data) : [];
|
||||||
|
if (op == 'delete')
|
||||||
|
properties.forEach(property => fields.push(property));
|
||||||
|
else {
|
||||||
|
const log = Model.definition.settings.log;
|
||||||
|
fields.push(idName);
|
||||||
|
if (log.relation) fields.push(Model.relations[log.relation].keyFrom);
|
||||||
|
if (log.showField) fields.push(log.showField);
|
||||||
|
else {
|
||||||
|
const showFieldNames = ['name', 'description', 'code', 'nickname'];
|
||||||
|
for (const field of showFieldNames) {
|
||||||
|
if (properties.includes(field)) {
|
||||||
|
log.showField = field;
|
||||||
|
fields.push(field);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stmt = new ParameterizedSQL(
|
||||||
|
'SELECT ' +
|
||||||
|
this.buildColumnNames(model, {fields}) +
|
||||||
|
' FROM ' +
|
||||||
|
this.tableEscaped(model)
|
||||||
|
);
|
||||||
|
stmt.merge(this.buildWhere(model, where));
|
||||||
|
if (limit) stmt.merge(`LIMIT 1`);
|
||||||
|
|
||||||
|
return stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLogRecord(oldInstances, newInstances, model, opts) {
|
||||||
|
function setActionType() {
|
||||||
|
if (oldInstances && newInstances)
|
||||||
|
return 'update';
|
||||||
|
else if (!oldInstances && newInstances)
|
||||||
|
return 'insert';
|
||||||
|
return 'delete';
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = setActionType();
|
||||||
|
if (!newInstances && action != 'delete') return;
|
||||||
|
|
||||||
|
const Model = this.getModelDefinition(model).model;
|
||||||
|
const models = Model.app.models;
|
||||||
|
const definition = Model.definition;
|
||||||
|
const log = definition.settings.log;
|
||||||
|
|
||||||
|
const primaryKey = this.idName(model);
|
||||||
|
const originRelation = log.relation;
|
||||||
|
const originFkField = originRelation
|
||||||
|
? Model.relations[originRelation].keyFrom
|
||||||
|
: primaryKey;
|
||||||
|
|
||||||
|
// Prevent adding logs when deleting a principal entity (Client, Zone...)
|
||||||
|
if (action == 'delete' && !originRelation) return;
|
||||||
|
|
||||||
|
function map(instances) {
|
||||||
|
const map = new Map();
|
||||||
|
if (!instances) return;
|
||||||
|
for (const instance of instances)
|
||||||
|
map.set(instance[primaryKey], instance);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedModel = definition.name;
|
||||||
|
const userFk = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
|
||||||
|
const oldMap = map(oldInstances);
|
||||||
|
const newMap = map(newInstances);
|
||||||
|
const ids = (oldMap || newMap).keys();
|
||||||
|
|
||||||
|
const logEntries = [];
|
||||||
|
|
||||||
|
function insertValuesLogEntry(logEntry, instance) {
|
||||||
|
logEntry.originFk = instance[originFkField];
|
||||||
|
logEntry.changedModelId = instance[primaryKey];
|
||||||
|
if (log.showField) logEntry.changedModelValue = instance[log.showField];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const oldI = oldMap && oldMap.get(id);
|
||||||
|
const newI = newMap && newMap.get(id);
|
||||||
|
|
||||||
|
const logEntry = {
|
||||||
|
action,
|
||||||
|
userFk,
|
||||||
|
changedModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newI) {
|
||||||
|
insertValuesLogEntry(logEntry, newI);
|
||||||
|
// Delete unchanged properties
|
||||||
|
if (oldI) {
|
||||||
|
Object.keys(oldI).forEach(prop => {
|
||||||
|
const hasChanges = oldI[prop] instanceof Date ?
|
||||||
|
oldI[prop]?.getTime() != newI[prop]?.getTime() :
|
||||||
|
oldI[prop] != newI[prop];
|
||||||
|
|
||||||
|
if (!hasChanges) {
|
||||||
|
delete oldI[prop];
|
||||||
|
delete newI[prop];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
insertValuesLogEntry(logEntry, oldI);
|
||||||
|
|
||||||
|
logEntry.oldInstance = oldI;
|
||||||
|
logEntry.newInstance = newI;
|
||||||
|
logEntries.push(logEntry);
|
||||||
|
}
|
||||||
|
await models[log.model].create(logEntries, opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.VnMySQL = VnMySQL;
|
exports.VnMySQL = VnMySQL;
|
||||||
|
|
|
@ -91,7 +91,11 @@ exports.getChanges = (original, changes) => {
|
||||||
const isPrivate = firstChar == '$';
|
const isPrivate = firstChar == '$';
|
||||||
if (isPrivate) return;
|
if (isPrivate) return;
|
||||||
|
|
||||||
if (changes[property] != original[property]) {
|
const hasChanges = original[property] instanceof Date ?
|
||||||
|
changes[property]?.getTime() != original[property]?.getTime() :
|
||||||
|
changes[property] != original[property];
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
newChanges[property] = changes[property];
|
newChanges[property] = changes[property];
|
||||||
|
|
||||||
if (original[property] != undefined)
|
if (original[property] != undefined)
|
||||||
|
|
|
@ -279,6 +279,18 @@ module.exports = Self => {
|
||||||
// Credit changes
|
// Credit changes
|
||||||
if (changes.credit !== undefined)
|
if (changes.credit !== undefined)
|
||||||
await Self.changeCredit(ctx, finalState, changes);
|
await Self.changeCredit(ctx, finalState, changes);
|
||||||
|
|
||||||
|
const oldInstance = {};
|
||||||
|
if (!ctx.isNewInstance) {
|
||||||
|
const newProps = Object.keys(changes);
|
||||||
|
Object.keys(orgData.__data).forEach(prop => {
|
||||||
|
if (newProps.includes(prop))
|
||||||
|
oldInstance[prop] = orgData[prop];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.hookState.oldInstance = oldInstance;
|
||||||
|
ctx.hookState.newInstance = changes;
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.observe('after save', async ctx => {
|
Self.observe('after save', async ctx => {
|
||||||
|
|
|
@ -165,18 +165,29 @@ module.exports = Self => {
|
||||||
'shipped',
|
'shipped',
|
||||||
'landed',
|
'landed',
|
||||||
'isDeleted',
|
'isDeleted',
|
||||||
'routeFk'
|
'routeFk',
|
||||||
|
'nickname'
|
||||||
],
|
],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
relation: 'client',
|
relation: 'client',
|
||||||
scope: {
|
scope: {
|
||||||
fields: 'salesPersonFk'
|
fields: 'salesPersonFk'
|
||||||
}
|
},
|
||||||
}]
|
include: [
|
||||||
|
{
|
||||||
|
relation: 'address',
|
||||||
|
scope: {
|
||||||
|
fields: 'nickname'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
}, myOptions);
|
}, myOptions);
|
||||||
|
|
||||||
args.routeFk = null;
|
args.routeFk = null;
|
||||||
|
if (args.isWithoutNegatives === false) delete args.isWithoutNegatives;
|
||||||
const updatedTicket = Object.assign({}, args);
|
const updatedTicket = Object.assign({}, args);
|
||||||
delete updatedTicket.ctx;
|
delete updatedTicket.ctx;
|
||||||
delete updatedTicket.option;
|
delete updatedTicket.option;
|
||||||
|
@ -224,37 +235,41 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const changes = loggable.getChanges(originalTicket, updatedTicket);
|
const changes = loggable.getChanges(originalTicket, updatedTicket);
|
||||||
const oldProperties = await loggable.translateValues(Self, changes.old);
|
const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0;
|
||||||
const newProperties = await loggable.translateValues(Self, changes.new);
|
|
||||||
|
|
||||||
await models.TicketLog.create({
|
if (hasChanges) {
|
||||||
originFk: args.id,
|
const oldProperties = await loggable.translateValues(Self, changes.old);
|
||||||
userFk: userId,
|
const newProperties = await loggable.translateValues(Self, changes.new);
|
||||||
action: 'update',
|
|
||||||
changedModel: 'Ticket',
|
|
||||||
changedModelId: args.id,
|
|
||||||
oldInstance: oldProperties,
|
|
||||||
newInstance: newProperties
|
|
||||||
}, myOptions);
|
|
||||||
|
|
||||||
const salesPersonId = originalTicket.client().salesPersonFk;
|
await models.TicketLog.create({
|
||||||
if (salesPersonId) {
|
originFk: args.id,
|
||||||
const origin = ctx.req.headers.origin;
|
userFk: userId,
|
||||||
|
action: 'update',
|
||||||
|
changedModel: 'Ticket',
|
||||||
|
changedModelId: args.id,
|
||||||
|
oldInstance: oldProperties,
|
||||||
|
newInstance: newProperties
|
||||||
|
}, myOptions);
|
||||||
|
|
||||||
let changesMade = '';
|
const salesPersonId = originalTicket.client().salesPersonFk;
|
||||||
for (let change in newProperties) {
|
if (salesPersonId) {
|
||||||
let value = newProperties[change];
|
const origin = ctx.req.headers.origin;
|
||||||
let oldValue = oldProperties[change];
|
|
||||||
|
|
||||||
changesMade += `\r\n~${$t(change)}: ${oldValue}~ ➔ *${$t(change)}: ${value}*`;
|
let changesMade = '';
|
||||||
|
for (let change in newProperties) {
|
||||||
|
let value = newProperties[change];
|
||||||
|
let oldValue = oldProperties[change];
|
||||||
|
|
||||||
|
changesMade += `\r\n~${$t(change)}: ${oldValue}~ ➔ *${$t(change)}: ${value}*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = $t('Changed this data from the ticket', {
|
||||||
|
ticketId: args.id,
|
||||||
|
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`,
|
||||||
|
changes: changesMade
|
||||||
|
});
|
||||||
|
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = $t('Changed this data from the ticket', {
|
|
||||||
ticketId: args.id,
|
|
||||||
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`,
|
|
||||||
changes: changesMade
|
|
||||||
});
|
|
||||||
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.id = args.id;
|
res.id = args.id;
|
||||||
|
|
|
@ -2,13 +2,13 @@ const models = require('vn-loopback/server/server').models;
|
||||||
const LoopBackContext = require('loopback-context');
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
describe('ticket merge()', () => {
|
describe('ticket merge()', () => {
|
||||||
const tickets = [{
|
const tickets = {
|
||||||
originId: 13,
|
originId: 13,
|
||||||
destinationId: 12,
|
destinationId: 12,
|
||||||
originShipped: Date.vnNew(),
|
originShipped: Date.vnNew(),
|
||||||
destinationShipped: Date.vnNew(),
|
destinationShipped: Date.vnNew(),
|
||||||
workerFk: 1
|
workerFk: 1
|
||||||
}];
|
};
|
||||||
|
|
||||||
const activeCtx = {
|
const activeCtx = {
|
||||||
accessToken: {userId: 9},
|
accessToken: {userId: 9},
|
||||||
|
@ -37,14 +37,14 @@ describe('ticket merge()', () => {
|
||||||
const options = {transaction: tx};
|
const options = {transaction: tx};
|
||||||
const chatNotificationBeforeMerge = await models.Chat.find();
|
const chatNotificationBeforeMerge = await models.Chat.find();
|
||||||
|
|
||||||
await models.Ticket.merge(ctx, tickets, options);
|
await models.Ticket.merge(ctx, [tickets], options);
|
||||||
|
|
||||||
const createdTicketLog = await models.TicketLog.find({where: {originFk: tickets[0].originId}}, options);
|
const createdTicketLog = await models.TicketLog.find({where: {originFk: tickets.originId}}, options);
|
||||||
const deletedTicket = await models.Ticket.findOne({where: {id: tickets[0].originId}}, options);
|
const deletedTicket = await models.Ticket.findOne({where: {id: tickets.originId}}, options);
|
||||||
const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets[0].destinationId}}, options);
|
const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets.destinationId}}, options);
|
||||||
const chatNotificationAfterMerge = await models.Chat.find();
|
const chatNotificationAfterMerge = await models.Chat.find();
|
||||||
|
|
||||||
expect(createdTicketLog.length).toEqual(1);
|
expect(createdTicketLog.length).toEqual(2);
|
||||||
expect(deletedTicket.isDeleted).toEqual(true);
|
expect(deletedTicket.isDeleted).toEqual(true);
|
||||||
expect(salesTicketFuture.length).toEqual(2);
|
expect(salesTicketFuture.length).toEqual(2);
|
||||||
expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2);
|
expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2);
|
||||||
|
|
|
@ -105,8 +105,8 @@ module.exports = Self => {
|
||||||
originFk: id,
|
originFk: id,
|
||||||
userFk: userId,
|
userFk: userId,
|
||||||
action: 'update',
|
action: 'update',
|
||||||
changedModel: 'Ticket',
|
changedModel: 'Sale',
|
||||||
changedModelId: id,
|
changedModelId: sale.id,
|
||||||
oldInstance: {
|
oldInstance: {
|
||||||
item: originalSaleData.itemFk,
|
item: originalSaleData.itemFk,
|
||||||
quantity: originalSaleData.quantity,
|
quantity: originalSaleData.quantity,
|
||||||
|
@ -126,8 +126,8 @@ module.exports = Self => {
|
||||||
originFk: ticketId,
|
originFk: ticketId,
|
||||||
userFk: userId,
|
userFk: userId,
|
||||||
action: 'update',
|
action: 'update',
|
||||||
changedModel: 'Ticket',
|
changedModel: 'Sale',
|
||||||
changedModelId: ticketId,
|
changedModelId: sale.id,
|
||||||
oldInstance: {
|
oldInstance: {
|
||||||
item: originalSaleData.itemFk,
|
item: originalSaleData.itemFk,
|
||||||
quantity: originalSaleData.quantity,
|
quantity: originalSaleData.quantity,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "salix-back",
|
"name": "salix-back",
|
||||||
"version": "23.10.01",
|
"version": "23.12.01",
|
||||||
"author": "Verdnatura Levante SL",
|
"author": "Verdnatura Levante SL",
|
||||||
"description": "Salix backend",
|
"description": "Salix backend",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|
Loading…
Reference in New Issue