refs #5423 añadido 3r decimal #1392

Merged
joan merged 4 commits from #5423-tercerDecimalCoste into dev 2023-03-21 10:22:39 +00:00
14 changed files with 398 additions and 388 deletions
Showing only changes of commit aadb796adf - Show all commits

View File

@ -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/),
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
### Added
- (Trabajadores -> Control de horario) Ahora se puede confirmar/no confirmar el registro horario de cada semana desde esta sección
### Changed
-
### Fixed
- (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

View File

View File

@ -81220,3 +81220,4 @@ USE `vn`;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2023-02-21 8:14:30

View File

@ -524,7 +524,7 @@ export default {
},
itemLog: {
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: {
header: 'vn-ticket-summary > vn-card > h5',

View File

@ -59,6 +59,6 @@ describe('Item log path', () => {
const fifthLineCreatedProperty = await page
.waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText');
expect(fifthLineCreatedProperty).toEqual('Coral y materiales similares');
expect(fifthLineCreatedProperty).toEqual('05080000');
});
});

View File

@ -37,6 +37,6 @@ describe('Zone descriptor path', () => {
await page.accessToSection('ticket.card.log');
const lastChanges = await page.waitToGetProperty(selectors.ticketLog.changes, 'innerText');
expect(lastChanges).toContain('Arreglar');
expect(lastChanges).toContain('1');
});
});

View File

@ -1,4 +1,3 @@
const pick = require('object.pick');
const LoopBackContext = require('loopback-context');
module.exports = function(Self) {
@ -6,344 +5,11 @@ module.exports = function(Self) {
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) {
const appModels = ctx.Model.app.models;
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;
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
});
Self.observe('before delete', async function(ctx) {
const appModels = ctx.Model.app.models;
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;
}
}
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
});
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';
}
};

View File

@ -2,8 +2,41 @@ const mysql = require('mysql');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const MySQL = require('loopback-connector-mysql').MySQL;
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
const Transaction = require('loopback-connector').Transaction;
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 {
/**
* Promisified version of execute().
@ -219,6 +252,277 @@ class VnMySQL extends MySQL {
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;

View File

@ -91,7 +91,11 @@ exports.getChanges = (original, changes) => {
const isPrivate = firstChar == '$';
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];
if (original[property] != undefined)

View File

@ -279,6 +279,18 @@ module.exports = Self => {
// Credit changes
if (changes.credit !== undefined)
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 => {

View File

@ -165,18 +165,29 @@ module.exports = Self => {
'shipped',
'landed',
'isDeleted',
'routeFk'
'routeFk',
'nickname'
],
include: [
{
relation: 'client',
scope: {
fields: 'salesPersonFk'
},
include: [
{
relation: 'address',
scope: {
fields: 'nickname'
}
}]
}
]
},
]
}, myOptions);
args.routeFk = null;
if (args.isWithoutNegatives === false) delete args.isWithoutNegatives;
const updatedTicket = Object.assign({}, args);
delete updatedTicket.ctx;
delete updatedTicket.option;
@ -224,6 +235,9 @@ module.exports = Self => {
}
const changes = loggable.getChanges(originalTicket, updatedTicket);
const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0;
if (hasChanges) {
const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new);
@ -256,6 +270,7 @@ module.exports = Self => {
});
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
}
}
res.id = args.id;
if (tx) await tx.commit();

View File

@ -2,13 +2,13 @@ const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket merge()', () => {
const tickets = [{
const tickets = {
originId: 13,
destinationId: 12,
originShipped: Date.vnNew(),
destinationShipped: Date.vnNew(),
workerFk: 1
}];
};
const activeCtx = {
accessToken: {userId: 9},
@ -37,14 +37,14 @@ describe('ticket merge()', () => {
const options = {transaction: tx};
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 deletedTicket = await models.Ticket.findOne({where: {id: tickets[0].originId}}, options);
const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets[0].destinationId}}, options);
const createdTicketLog = await models.TicketLog.find({where: {originFk: tickets.originId}}, options);
const deletedTicket = await models.Ticket.findOne({where: {id: tickets.originId}}, options);
const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets.destinationId}}, options);
const chatNotificationAfterMerge = await models.Chat.find();
expect(createdTicketLog.length).toEqual(1);
expect(createdTicketLog.length).toEqual(2);
expect(deletedTicket.isDeleted).toEqual(true);
expect(salesTicketFuture.length).toEqual(2);
expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2);

View File

@ -105,8 +105,8 @@ module.exports = Self => {
originFk: id,
userFk: userId,
action: 'update',
changedModel: 'Ticket',
changedModelId: id,
changedModel: 'Sale',
changedModelId: sale.id,
oldInstance: {
item: originalSaleData.itemFk,
quantity: originalSaleData.quantity,
@ -126,8 +126,8 @@ module.exports = Self => {
originFk: ticketId,
userFk: userId,
action: 'update',
changedModel: 'Ticket',
changedModelId: ticketId,
changedModel: 'Sale',
changedModelId: sale.id,
oldInstance: {
item: originalSaleData.itemFk,
quantity: originalSaleData.quantity,

View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.10.01",
"version": "23.12.01",
"author": "Verdnatura Levante SL",
"description": "Salix backend",
"license": "GPL-3.0",