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

This commit is contained in:
Javi Gallego 2020-12-29 17:00:45 +01:00
commit 0d5540b639
49 changed files with 654 additions and 50 deletions

14
Jenkinsfile vendored
View File

@ -69,13 +69,13 @@ pipeline {
}
}
}
/* stage('Backend') {
steps {
nodejs('node-lts') {
sh 'gulp launchBackTest --ci'
}
}
} */
// stage('Backend') {
// steps {
// nodejs('node-lts') {
// sh 'gulp launchBackTest --ci'
// }
// }
// }
}
}
stage('Build') {

View File

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

View File

@ -0,0 +1,14 @@
CREATE TABLE `vn`.`entryObservation` (
id int NOT NULL AUTO_INCREMENT,
entryFk int NOT NULL,
observationTypeFk TINYINT(3) UNSIGNED,
description TEXT,
PRIMARY KEY (id),
CONSTRAINT entry_id_entryFk
FOREIGN KEY (entryFk) REFERENCES entry(id),
CONSTRAINT observationType_id_observationTypeFk
FOREIGN KEY (observationTypeFk) REFERENCES observationType(id)
);
ALTER TABLE `vn`.`entryObservation`
ADD UNIQUE INDEX `entryFk_observationTypeFk_UNIQUE` (`entryFk` ASC,`observationTypeFk` ASC);

View File

@ -0,0 +1,18 @@
CREATE TABLE `vn`.`zoneLog` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`originFk` int(10) NOT NULL,
`userFk` int(10) unsigned DEFAULT NULL,
`action` set('insert','update','delete') COLLATE utf8_unicode_ci NOT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(),
`description` text CHARACTER SET utf8 DEFAULT NULL,
`changedModel` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
`oldInstance` text COLLATE utf8_unicode_ci DEFAULT NULL,
`newInstance` text COLLATE utf8_unicode_ci DEFAULT NULL,
`changedModelId` int(11) DEFAULT NULL,
`changedModelValue` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `originFk` (`originFk`),
KEY `userFk` (`userFk`),
CONSTRAINT `zoneLog_ibfk_1` FOREIGN KEY (`originFk`) REFERENCES `vn`.`zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `zoneLog_ibfk_2` FOREIGN KEY (`userFk`) REFERENCES `account`.`user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

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

View File

@ -1639,7 +1639,7 @@ INSERT INTO `vn`.`orderTicket`(`orderFk`, `ticketFk`)
INSERT INTO `vn`.`userConfig` (`userFk`, `warehouseFk`, `companyFk`)
VALUES
(1, 2, 69),
(1, 1, 69),
(5, 1, 442),
(9, 1, 442),
(18, 3, 567);

View File

@ -300,9 +300,14 @@ let actions = {
},
waitForNumberOfElements: async function(selector, count) {
return await this.waitForFunction((selector, count) => {
return document.querySelectorAll(selector).length == count;
}, {}, selector, count);
try {
await this.waitForFunction((selector, count) => {
return document.querySelectorAll(selector).length == count;
}, {}, selector, count);
} catch (error) {
const amount = await this.countElement(selector);
throw new Error(`actual amount of elements was: ${amount} instead of ${count}, ${error}`);
}
},
waitForClassNotPresent: async function(selector, className) {

View File

@ -674,7 +674,14 @@ export default {
confirmButton: '.vn-confirm.shown button[response="accept"]',
},
routeIndex: {
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]'
anyResult: 'vn-table a',
firstRouteCheckbox: 'a:nth-child(1) vn-td:nth-child(1) > vn-check',
addNewRouteButton: 'vn-route-index a[ui-sref="route.create"]',
cloneButton: 'vn-route-index button > vn-icon[icon="icon-clone"]',
submitClonationButton: 'tpl-buttons > button[response="accept"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
searchAgencyAutocomlete: 'vn-route-search-panel vn-autocomplete[ng-model="filter.agencyModeFk"]',
advancedSearchButton: 'vn-route-search-panel button[type=submit]',
},
createRouteView: {
worker: 'vn-route-create vn-autocomplete[ng-model="$ctrl.route.workerFk"]',

View File

@ -71,7 +71,7 @@ describe('User config', () => {
expect(expectedLocalWarehouse).toBeTruthy();
expect(expectedLocalBank).toBeTruthy();
expect(expectedLocalCompany).toBeTruthy();
expect(userWarehouse).toEqual('Warehouse Two');
expect(userWarehouse).toEqual('Warehouse One');
expect(userCompany).toEqual('CCs');
});

View File

@ -57,5 +57,36 @@ describe('Route create path', () => {
it(`should confirm the redirection to the created route summary`, async() => {
await page.waitForState('route.card.summary');
});
it(`should navigate back to the route index`, async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitForState('route.index');
});
let count;
it(`should count the amount of routes before clonation`, async() => {
await page.waitForFunction(selector => {
return document.querySelectorAll(selector).length > 6;
}, {}, selectors.routeIndex.anyResult);
count = await page.countElement(selectors.routeIndex.anyResult);
expect(count).toBeGreaterThanOrEqual(7);
});
it(`should clone the first route`, async() => {
await page.waitToClick(selectors.routeIndex.firstRouteCheckbox);
await page.waitToClick(selectors.routeIndex.cloneButton);
await page.waitToClick(selectors.routeIndex.submitClonationButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should reload the section and count the amount of routes after clonation`, async() => {
await page.waitForNumberOfElements(selectors.routeIndex.anyResult, count + 1);
const result = await page.countElement(selectors.routeIndex.anyResult);
expect(result).toEqual(count + 1);
});
});
});

View File

@ -4,6 +4,8 @@ import './style.scss';
export default class DescriptorPopover extends Popover {
show(parent, id) {
if (!id) return;
super.show(parent);
this.id = id;

View File

@ -145,9 +145,15 @@ module.exports = function(Self) {
rewriteDbError(replaceErrFunc) {
function replaceErr(err, replaceErrFunc) {
if (Array.isArray(err)) {
const errors = err.filter(error => {
return error != undefined && error != null;
});
let errs = [];
for (let e of err)
errs.push(replaceErrFunc(e));
for (let e of errors) {
if (!(e instanceof UserError))
errs.push(replaceErrFunc(e));
else errs.push(e);
}
return errs;
}
return replaceErrFunc(err);

View File

@ -246,3 +246,98 @@ exports.initialize = function initialize(dataSource, callback) {
dataSource.connector.connect(callback);
}
};
MySQL.prototype.connect = function(callback) {
const self = this;
const options = generateOptions(this.settings);
if (this.client) {
if (callback) {
process.nextTick(function() {
callback(null, self.client);
});
}
} else
this.client = connectionHandler(options, callback);
function connectionHandler(options, callback) {
const client = mysql.createPool(options);
client.getConnection(function(err, connection) {
const conn = connection;
if (!err) {
if (self.debug)
debug('MySQL connection is established: ' + self.settings || {});
connection.release();
} else {
if (err.code == 'ECONNREFUSED' || err.code == 'PROTOCOL_CONNECTION_LOST') { // PROTOCOL_CONNECTION_LOST
console.error(`MySQL connection lost (${err.code}). Retrying..`);
return setTimeout(() =>
connectionHandler(options, callback), 5000);
}
if (self.debug || !callback)
console.error('MySQL connection is failed: ' + self.settings || {}, err);
}
callback && callback(err, conn);
});
return client;
}
};
function generateOptions(settings) {
const s = settings || {};
if (s.collation) {
// Charset should be first 'chunk' of collation.
s.charset = s.collation.substr(0, s.collation.indexOf('_'));
} else {
s.collation = 'utf8_general_ci';
s.charset = 'utf8';
}
s.supportBigNumbers = (s.supportBigNumbers || false);
s.timezone = (s.timezone || 'local');
if (isNaN(s.connectionLimit))
s.connectionLimit = 10;
let options;
if (s.url) {
// use url to override other settings if url provided
options = s.url;
} else {
options = {
host: s.host || s.hostname || 'localhost',
port: s.port || 3306,
user: s.username || s.user,
password: s.password,
timezone: s.timezone,
socketPath: s.socketPath,
charset: s.collation.toUpperCase(), // Correct by docs despite seeming odd.
supportBigNumbers: s.supportBigNumbers,
connectionLimit: s.connectionLimit,
};
// Don't configure the DB if the pool can be used for multiple DBs
if (!s.createDatabase)
options.database = s.database;
// Take other options for mysql driver
// See https://github.com/strongloop/loopback-connector-mysql/issues/46
for (const p in s) {
if (p === 'database' && s.createDatabase)
continue;
if (options[p] === undefined)
options[p] = s[p];
}
// Legacy UTC Date Processing fallback - SHOULD BE TRANSITIONAL
if (s.legacyUtcDateProcessing === undefined)
s.legacyUtcDateProcessing = true;
if (s.legacyUtcDateProcessing)
options.timezone = 'Z';
}
return options;
}

View File

@ -7,5 +7,8 @@
},
"EntryLog": {
"dataSource": "vn"
},
"EntryObservation": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,9 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`The observation type can't be repeated`);
return err;
});
};

View File

@ -0,0 +1,37 @@
{
"name": "EntryObservation",
"base": "Loggable",
"log": {
"model": "EntryLog",
"relation": "entry"
},
"options": {
"mysql": {
"table": "entryObservation"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"description": "Identifier"
},
"description": {
"type": "String",
"required": true
}
},
"relations": {
"entry": {
"type": "belongsTo",
"model": "Entry",
"foreignKey": "entryFk"
},
"observationType": {
"type": "belongsTo",
"model": "ObservationType",
"foreignKey": "observationTypeFk",
"required": true
}
}
}

View File

@ -10,6 +10,7 @@ import './latest-buys-search-panel';
import './descriptor';
import './descriptor-popover';
import './card';
import './note';
import './summary';
import './log';
import './buy';

View File

@ -0,0 +1,72 @@
<vn-crud-model
vn-id="model"
url="EntryObservations"
fields="['id', 'entryFk', 'observationTypeFk', 'description']"
link="{entryFk: $ctrl.$params.id}"
data="observations"
auto-load="true">
</vn-crud-model>
<vn-crud-model
url="ObservationTypes"
data="observationTypes"
auto-load="true">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="observations"
form="form">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-one class="vn-mt-md">
<vn-horizontal ng-repeat="observation in observations track by $index">
<vn-autocomplete
ng-attr-disabled="observation.id ? true : false"
ng-model="observation.observationTypeFk"
data="observationTypes"
show-field="description"
label="Observation type"
vn-one
vn-focus>
</vn-autocomplete>
<vn-textfield
vn-two
class="vn-mr-lg"
label="Description"
ng-model="observation.description"
rule="EntryObservation">
</vn-textfield>
<vn-auto class="vn-pt-md">
<vn-icon-button
pointer
vn-tooltip="Remove note"
icon="delete"
ng-click="model.remove($index)">
</vn-icon-button>
</vn-auto>
</vn-horizontal>
</vn-one>
<vn-one>
<vn-icon-button
vn-tooltip="Add note"
icon="add_circle"
vn-bind="+"
ng-if="observationTypes.length > observations.length"
ng-click="model.insert()">
</vn-icon-button>
</vn-one>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<!-- # #2680 Undo changes button bugs -->
<!-- <vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button> -->
</vn-button-bar>
</form>

View File

@ -0,0 +1,20 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
onSubmit() {
this.$.watcher.check();
this.$.model.save().then(() => {
this.$.watcher.notifySaved();
this.$.watcher.updateOriginalData();
});
}
}
ngModule.vnComponent('vnEntryObservation', {
template: require('./index.html'),
controller: Controller,
bindings: {
entry: '<'
}
});

View File

@ -12,6 +12,7 @@
"card": [
{"state": "entry.card.basicData", "icon": "settings"},
{"state": "entry.card.buy", "icon": "icon-lines"},
{"state": "entry.card.observation", "icon": "insert_drive_file"},
{"state": "entry.card.log", "icon": "history"}
]
},
@ -61,7 +62,15 @@
"params": {
"entry": "$ctrl.entry"
}
}, {
},{
"url": "/observation",
"state": "entry.card.observation",
"component": "vn-entry-observation",
"description": "Notes",
"params": {
"entry": "$ctrl.entry"
}
},{
"url" : "/log",
"state": "entry.card.log",
"component": "vn-entry-log",

View File

@ -0,0 +1,56 @@
module.exports = Self => {
Self.remoteMethod('clone', {
description: 'Clones the selected routes',
accessType: 'WRITE',
accepts: [
{
arg: 'ids',
type: ['number'],
required: true,
description: 'The routes ids to clone'
},
{
arg: 'started',
type: 'date',
required: true,
description: 'The started date for all routes'
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/clone`,
verb: 'POST'
}
});
Self.clone = async(ids, started) => {
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const originalRoutes = await Self.find({
where: {id: {inq: ids}},
fields: ['workerFk', 'agencyModeFk', 'vehicleFk', 'description']
}, options);
if (ids.length != originalRoutes.length)
throw new Error(`The amount of routes found don't match`);
const routes = originalRoutes.map(route => {
route.started = started;
route.created = new Date();
return route;
});
const clones = await Self.create(routes, options);
await tx.commit();
return clones;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,31 @@
const app = require('vn-loopback/server/server');
describe('route clone()', () => {
const startDate = new Date();
it('should throw an error if the amount of ids pased to the clone function do no match the database', async() => {
const ids = [996, 997, 998, 999];
let error;
try {
await app.models.Route.clone(ids, startDate);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual(`The amount of routes found don't match`);
});
it('should clone two routes', async() => {
const ids = [1, 2];
const clones = await app.models.Route.clone(ids, startDate);
expect(clones.length).toEqual(2);
// restores
for (const clone of clones)
await app.models.Route.destroyById(clone.id);
});
});

View File

@ -6,6 +6,7 @@ module.exports = Self => {
require('../methods/route/updateVolume')(Self);
require('../methods/route/getDeliveryPoint')(Self);
require('../methods/route/insertTicket')(Self);
require('../methods/route/clone')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'

View File

@ -3,3 +3,5 @@ Date started: Fecha inicio
Km start: Km de inicio
Km end: Km de fin
Description: Descripción
Hour started: Hora inicio
Hour finished: Hora fin

View File

@ -44,7 +44,7 @@
</vn-td>
<vn-td>{{::route.agencyName | dashIfEmpty}}</vn-td>
<vn-td>{{::route.vehiclePlateNumber | dashIfEmpty}}</vn-td>
<vn-td expand>{{::route.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td expand>{{::route.created | dashIfEmpty | date:'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::route.m3 | dashIfEmpty}}</vn-td>
<vn-td>{{::route.description | dashIfEmpty}}</vn-td>
<vn-td>
@ -59,19 +59,26 @@
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-popup vn-id="summary">
<vn-route-summary
route="$ctrl.routeSelected">
</vn-route-summary>
</vn-popup>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
</vn-data-viewer>
<div fixed-bottom-right>
<vn-vertical style="align-items: center;">
<vn-button class="round sm vn-mb-sm"
icon="icon-clone"
ng-show="$ctrl.totalChecked > 0"
ng-click="$ctrl.openClonationDialog()"
vn-tooltip="Clone selected routes"
tooltip-position="left">
</vn-button>
<vn-button class="round sm vn-mb-sm"
icon="cloud_download"
ng-show="$ctrl.totalChecked > 0"
@ -79,7 +86,6 @@
vn-tooltip="Download selected routes as PDF"
tooltip-position="left">
</vn-button>
<a ui-sref="route.create" vn-bind="+">
<vn-button class="round md vn-mb-sm"
icon="add"
@ -88,4 +94,24 @@
</vn-button>
</a>
</vn-vertical>
</div>
</div>
<!-- Clonation dialog -->
<vn-dialog class="edit"
vn-id="clonationDialog"
on-accept="$ctrl.cloneSelectedRoutes()"
message="Select the starting date">
<tpl-body>
<vn-horizontal>
<vn-date-picker
label="Starting date"
ng-model="$ctrl.startedDate"
required="true">
</vn-date-picker>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Clone</button>
</tpl-buttons>
</vn-dialog>

View File

@ -40,6 +40,22 @@ export default class Controller extends Section {
});
}
openClonationDialog() {
this.startedDate = new Date();
this.$.clonationDialog.show();
}
cloneSelectedRoutes() {
const routesIds = [];
for (let route of this.checked)
routesIds.push(route.id);
return this.$http.post('Routes/clone', {ids: routesIds, started: this.startedDate}).then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
onDrop($event) {
const target = $event.target;
const droppable = target.closest(this.droppableElement);

View File

@ -60,6 +60,16 @@ describe('Component vnRouteIndex', () => {
});
});
describe('cloneSelectedRoutes()', () => {
it('should perform an http request to Routes/clone', () => {
controller.startedDate = new Date();
$httpBackend.expect('POST', 'Routes/clone').respond();
controller.cloneSelectedRoutes();
$httpBackend.flush();
});
});
describe('onDrop()', () => {
it('should call the insert method when dragging a ticket number', () => {
jest.spyOn(controller, 'insert');

View File

@ -1,2 +1,5 @@
Vehicle: Vehículo
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Download selected routes as PDF: Descargar rutas seleccionadas como PDF
Clone selected routes: Clonar rutas seleccionadas
Select the starting date: Seleccione fecha de inicio
Starting date: Fecha de inicio

View File

@ -2,15 +2,14 @@
vn-id="model"
url="Routes/filter"
limit="20"
order="created DESC"
auto-load="true">
order="created DESC">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-route-search-panel"
info="Search routes by id"
filter="$ctrl.filter"
filter="$ctrl.filterParams"
model="model">
</vn-searchbar>
</vn-portal>

View File

@ -10,7 +10,8 @@ export default class Route extends ModuleMain {
let from = new Date();
from.setHours(0, 0, 0, 0);
this.filter = {from, to, warehouseFk: this.vnConfig.warehouseFk};
this.filterParams = {from, to, warehouseFk: this.vnConfig.warehouseFk};
this.$.model.applyFilter(null, this.filterParams);
}
}

View File

@ -25,8 +25,7 @@
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk",
"required": true
"foreignKey": "ticketFk"
},
"observationType": {
"type": "belongsTo",

View File

@ -11,7 +11,7 @@
info="Search ticket by id or alias"
model="model"
fetch-params="$ctrl.fetchParams($params)"
suggested-filter="$ctrl.defaultFilter">
suggested-filter="$ctrl.filterParams">
</vn-searchbar>
</vn-portal>
<vn-portal slot="menu">

View File

@ -5,7 +5,7 @@ export default class Ticket extends ModuleMain {
constructor() {
super();
this.defaultFilter = {
this.filterParams = {
scopeDays: 1
};
}

View File

@ -23,9 +23,9 @@
<vn-td>{{::tracking.state.name}}</vn-td>
<vn-td expand>
<span
class="link"
ng-class="{'link': tracking.worker.id}"
ng-click="workerDescriptor.show($event, tracking.worker.user.id)">
{{::tracking.worker.user.name | dashIfEmpty}}
{{::tracking.worker.user.name || 'System' | translate}}
</span>
</vn-td>
<vn-td>{{::tracking.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>

View File

@ -10,7 +10,8 @@
panel="vn-travel-search-panel"
info="Search travels by id"
model="model"
fetch-params="$ctrl.fetchParams($params)">
fetch-params="$ctrl.fetchParams($params)"
suggested-filter="$ctrl.filterParams">
</vn-searchbar>
</vn-portal>
<vn-portal slot="menu">

View File

@ -2,6 +2,14 @@ import ngModule from '../module';
import ModuleMain from 'salix/components/module-main';
export default class Travel extends ModuleMain {
constructor() {
super();
this.filterParams = {
scopeDays: 1
};
}
fetchParams($params) {
if (!Object.entries($params).length)
$params.scopeDays = 1;

View File

@ -33,7 +33,7 @@
<span
ng-class="{'link': log.user.worker.id, 'value': !log.user.worker.id}"
ng-click="$ctrl.showWorkerDescriptor($event, log.user.worker.id)"
translate>{{::log.user.name | dashIfEmpty}}
translate>{{::log.user.name || 'System' | translate}}
</span>
</div>
<div>
@ -54,7 +54,7 @@
<span
ng-class="{'link': log.user.worker.id, 'value': !log.user.worker.id}"
ng-click="$ctrl.showWorkerDescriptor($event, log.user.worker.id)"
translate>{{::log.user.name | dashIfEmpty}}
translate>{{::log.user.name || 'System' | translate}}
</span>
</vn-td>
<vn-td class="expendable">
@ -70,7 +70,7 @@
<vn-one ng-repeat="old in log.oldProperties">
<div>
<span translate class="label">{{::old.key}}</span><span class="label">: </span>
<span translate class="value">{{::old.value}}</span>
<span translate class="value">{{::old.value | dashIfEmpty}}</span>
</div>
</vn-one>
</vn-td>
@ -81,7 +81,7 @@
id="newInstance">
<div>
<span translate class="label">{{::new.key}}</span><span class="label">: </span>
<span translate class="value">{{::new.value}}</span>
<span translate class="value">{{::new.value | dashIfEmpty}}</span>
</div>
</vn-one>
<vn-one

View File

@ -9,4 +9,5 @@ Name: Nombre
Creates: Crea
Updates: Actualiza
Deletes: Elimina
Views: Visualiza
Views: Visualiza
System: Sistema

View File

@ -31,5 +31,8 @@
},
"ZoneEstimatedDelivery": {
"dataSource": "vn"
},
"ZoneLog": {
"dataSource": "vn"
}
}

View File

@ -1,6 +1,10 @@
{
"name": "ZoneEvent",
"base": "VnModel",
"base": "Loggable",
"log": {
"model":"ZoneLog",
"relation": "zone"
},
"options": {
"mysql": {
"table": "zoneEvent"

View File

@ -1,6 +1,11 @@
{
"name": "ZoneIncluded",
"base": "VnModel",
"base": "Loggable",
"log": {
"model": "ZoneLog",
"relation": "zone",
"showField": "isIncluded"
},
"options": {
"mysql": {
"table": "zoneIncluded"

View File

@ -0,0 +1,58 @@
{
"name": "ZoneLog",
"base": "VnModel",
"options": {
"mysql": {
"table": "zoneLog"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"forceId": false
},
"originFk": {
"type": "Number",
"required": true
},
"userFk": {
"type": "Number"
},
"action": {
"type": "String",
"required": true
},
"changedModel": {
"type": "String"
},
"oldInstance": {
"type": "Object"
},
"newInstance": {
"type": "Object"
},
"creationDate": {
"type": "Date"
},
"changedModelId": {
"type": "String"
},
"changedModelValue": {
"type": "String"
},
"description": {
"type": "String"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,6 +1,10 @@
{
"name": "ZoneWarehouse",
"base": "VnModel",
"base": "Loggable",
"log": {
"model":"ZoneLog",
"relation": "zone"
},
"options": {
"mysql": {
"table": "zoneWarehouse"

View File

@ -1,6 +1,10 @@
{
"name": "Zone",
"base": "VnModel",
"base": "Loggable",
"log": {
"model":"ZoneLog",
"showField": "name"
},
"options": {
"mysql": {
"table": "zone"
@ -39,11 +43,6 @@
}
},
"relations": {
"geolocations": {
"type": "hasMany",
"model": "ZoneGeo",
"foreignKey": "zoneFk"
},
"agencyMode": {
"type": "belongsTo",
"model": "AgencyMode",

View File

@ -16,3 +16,4 @@ import './calendar';
import './location';
import './calendar';
import './upcoming-deliveries';
import './log';

View File

@ -0,0 +1 @@
<vn-log url="ZoneLogs" origin-id="$ctrl.$params.id"></vn-log>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
ngModule.vnComponent('vnZoneLog', {
template: require('./index.html'),
controller: Section,
});

View File

@ -14,6 +14,7 @@
{"state": "zone.card.basicData", "icon": "settings"},
{"state": "zone.card.location", "icon": "my_location"},
{"state": "zone.card.warehouses", "icon": "home"},
{"state": "zone.card.log", "icon": "history"},
{"state": "zone.card.events", "icon": "today"}
]
},
@ -84,6 +85,11 @@
"params": {
"zone": "$ctrl.zone"
}
}, {
"url" : "/log",
"state": "zone.card.log",
"component": "vn-zone-log",
"description": "Log"
}
]
}

View File

@ -151,6 +151,7 @@ module.exports = app => {
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
@ -196,6 +197,10 @@ module.exports = app => {
const email = new Email('delivery-note-link', args);
await email.send();
} catch (error) {
// Domain not found
if (error.responseCode == 450)
return invalidEmail(ticket);
// Save tickets on a list of failed ids
failedtickets.push({
id: ticket.id,
@ -220,4 +225,33 @@ module.exports = app => {
});
}
}
async function invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = :clientId`, {
clientId: ticket.clientFk
});
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (:clientId, :userId, 'UPDATE', 'Client', :oldInstance, :newInstance)`, {
clientId: ticket.clientFk,
userId: null,
oldInstance: oldInstance,
newInstance: newInstance
});
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};