refs #5128 añadida subseccion "Gestión de crédito"
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Vicent Llopis 2023-04-11 14:57:56 +02:00
parent 5ab2913c39
commit d03ca01b73
15 changed files with 383 additions and 72 deletions

View File

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

View File

@ -0,0 +1,16 @@
ALTER TABLE `vn`.`client` ADD rating INT UNSIGNED DEFAULT NULL NULL COMMENT 'información proporcionada por Informa';
ALTER TABLE `vn`.`client` ADD recommendedCredit INT UNSIGNED DEFAULT NULL NULL COMMENT 'información proporcionada por Informa';
CREATE TABLE `vn`.`clientInforma` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`clientFk` int(11) NOT NULL,
`rating` int(10) unsigned DEFAULT NULL,
`recommendedCredit` int(10) unsigned DEFAULT NULL,
`workerFk` int(10) unsigned NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `informaWorkers_fk_idx` (`workerFk`),
KEY `informaClientFk` (`clientFk`),
CONSTRAINT `informa_ClienteFk` FOREIGN KEY (`clientFk`) REFERENCES `client` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `informa_workers_fk` FOREIGN KEY (`workerFk`) REFERENCES `worker` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

View File

@ -32,6 +32,9 @@
"ClientConsumptionQueue": { "ClientConsumptionQueue": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ClientInforma": {
"dataSource": "vn"
},
"ClientLog": { "ClientLog": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,42 @@
{
"name": "ClientInforma",
"base": "Loggable",
"log": {
"model":"ClientLog",
"relation": "client",
"showField": "clientFk"
},
"options": {
"mysql": {
"table": "clientInforma"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"rating": {
"type": "number"
},
"recommendedCredit": {
"type": "number"
},
"created": {
"type": "date"
}
},
"relations": {
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"client": {
"type": "belongsTo",
"model": "Client",
"foreignKey": "clientFk"
}
}
}

View File

@ -280,6 +280,10 @@ module.exports = Self => {
if (changes.credit !== undefined) if (changes.credit !== undefined)
await Self.changeCredit(ctx, finalState, changes); await Self.changeCredit(ctx, finalState, changes);
// Credit management changes
if (orgData.rating != changes.rating || orgData.recommendedCredit != changes.recommendedCredit)
await Self.changeCreditManagement(ctx, finalState, changes);
const oldInstance = {}; const oldInstance = {};
if (!ctx.isNewInstance) { if (!ctx.isNewInstance) {
const newProps = Object.keys(changes); const newProps = Object.keys(changes);
@ -441,6 +445,55 @@ module.exports = Self => {
}, ctx.options); }, ctx.options);
}; };
Self.changeCreditManagement = async function changeCreditManagement(ctx, finalState, changes) {
const models = Self.app.models;
const userId = ctx.options.accessToken.userId;
// const isFinancialBoss = await models.Account.hasRole(userId, 'financialBoss', ctx.options);
// if (!isFinancialBoss) {
// const lastCredit = await models.ClientCredit.findOne({
// where: {
// clientFk: finalState.id
// },
// order: 'id DESC'
// }, ctx.options);
// const lastAmount = lastCredit && lastCredit.amount;
// const lastWorkerId = lastCredit && lastCredit.workerFk;
// const lastWorkerIsFinancialBoss = await models.Account.hasRole(lastWorkerId, 'financialBoss', ctx.options);
// if (lastAmount == 0 && lastWorkerIsFinancialBoss)
// throw new UserError(`You can't change the credit set to zero from a financialBoss`);
// const creditLimits = await models.ClientCreditLimit.find({
// fields: ['roleFk'],
// where: {
// maxAmount: {gte: changes.credit}
// }
// }, ctx.options);
// const requiredRoles = [];
// for (limit of creditLimits)
// requiredRoles.push(limit.roleFk);
// const userRequiredRoles = await models.RoleMapping.count({
// roleId: {inq: requiredRoles},
// principalType: 'USER',
// principalId: userId
// }, ctx.options);
// if (userRequiredRoles <= 0)
// throw new UserError(`You don't have enough privileges to set this credit amount`);
// }
await models.ClientInforma.create({
clientFk: finalState.id,
rating: changes.rating,
recommendedCredit: changes.recommendedCredit,
workerFk: userId
}, ctx.options);
};
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
app.on('started', function() { app.on('started', function() {
const account = app.models.Account; const account = app.models.Account;
@ -474,7 +527,8 @@ module.exports = Self => {
oldInstance: {name: oldData.name, active: oldData.active}, oldInstance: {name: oldData.name, active: oldData.active},
newInstance: {name: changes.name, active: changes.active} newInstance: {name: changes.name, active: changes.active}
}; };
await Self.app.models.ClientLog.create(logRecord); console.log(logRecord);
// await Self.app.models.ClientLog.create(logRecord);
} }
} }
}); });

View File

@ -145,6 +145,12 @@
}, },
"hasElectronicInvoice": { "hasElectronicInvoice": {
"type": "boolean" "type": "boolean"
},
"rating": {
"type": "number"
},
"recommendedCredit": {
"type": "number"
} }
}, },

View File

@ -0,0 +1,91 @@
<mg-ajax path="Clients/{{patch.params.id}}" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
url="Clients"
data="$ctrl.client"
id-value="$ctrl.$params.id"
form="form"
save="patch">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-input-number
vn-one
label="Rating"
ng-model="$ctrl.client.rating"
vn-focus
rule>
</vn-input-number>
<vn-input-number
vn-one
label="Recommended credit"
ng-model="$ctrl.client.recommendedCredit"
rule>
</vn-input-number>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
<vn-button
ng-click="$ctrl.cancel()"
label="Cancel">
</vn-button>
</vn-button-bar>
</form>
<vn-crud-model
vn-id="model"
url="ClientInformas"
filter="$ctrl.filter"
link="{clientFk: $ctrl.$params.id}"
limit="20"
data="clientInformas"
order="created DESC"
auto-load="true">
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-md">
<vn-card>
<vn-table model="model" class="vn-mt-lg">
<vn-thead>
<vn-tr>
<vn-th shrink-date field="created">Since</vn-th>
<vn-th field="workerFk">Employee</vn-th>
<vn-th field="rating" number>Rating</vn-th>
<vn-th field="recommendedCredit" number>Recommended credit</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="clientInforma in clientInformas">
<vn-td shrink-datetime>{{::clientInforma.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td shrink>
<span
ng-click="workerDescriptor.show($event, clientInforma.workerFk)"
class="link">
{{::clientInforma.worker.user.nickname}}
</span>
</vn-td>
<vn-td number>{{::clientInforma.rating}}</vn-td>
<vn-td number>{{::clientInforma.recommendedCredit}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-float-button
icon="add"
ui-sref="client.card.credit.create"
vn-acl="teamBoss"
vn-acl-action="remove"
vn-tooltip="New credit"
vn-bind="+"
fixed-bottom-right>
</vn-float-button>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,32 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
include: [{
relation: 'worker',
scope: {
fields: ['userFk'],
include: {
relation: 'user',
scope: {
fields: ['nickname']
}
}
}
}],
};
}
onSubmit() {
this.$.watcher.submit()
.then(() => this.$state.reload());
}
}
ngModule.vnComponent('vnClientCreditManagement', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,38 @@
import './index';
describe('client unpaid', () => {
describe('Component vnClientUnpaid', () => {
let controller;
beforeEach(ngModule('client'));
beforeEach(inject($componentController => {
const $element = angular.element('<vn-client-unpaid></vn-client-unpaid>');
controller = $componentController('vnClientUnpaid', {$element});
}));
describe('setDefaultDate()', () => {
it(`should not set today date if has dated`, () => {
const hasData = true;
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
controller.clientUnpaid = {
dated: yesterday
};
controller.setDefaultDate(hasData);
expect(controller.clientUnpaid.dated).toEqual(yesterday);
});
it(`should set today if not has dated`, () => {
const hasData = true;
controller.clientUnpaid = {};
controller.setDefaultDate(hasData);
expect(controller.clientUnpaid.dated).toBeDefined();
});
});
});
});

View File

@ -0,0 +1,2 @@
Recommended credit: Crédito recomendado
Rating: Clasificación

View File

@ -47,3 +47,5 @@ import './defaulter';
import './notification'; import './notification';
import './unpaid'; import './unpaid';
import './extended-list'; import './extended-list';
import './credit-management';

View File

@ -64,3 +64,4 @@ Compensation Account: Cuenta para compensar
Amount to return: Cantidad a devolver Amount to return: Cantidad a devolver
Delivered amount: Cantidad entregada Delivered amount: Cantidad entregada
Unpaid: Impagado Unpaid: Impagado
Credit management: Gestión de crédito

View File

@ -34,7 +34,8 @@
{"state": "client.card.contact", "icon": "contact_phone"}, {"state": "client.card.contact", "icon": "contact_phone"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"}, {"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms.index", "icon": "cloud_upload"}, {"state": "client.card.dms.index", "icon": "cloud_upload"},
{"state": "client.card.unpaid", "icon": "icon-defaulter"} {"state": "client.card.unpaid", "icon": "icon-defaulter"},
{"state": "client.card.creditManagement", "icon": "contact_support"}
] ]
} }
] ]
@ -416,7 +417,8 @@
"state": "client.notification", "state": "client.notification",
"component": "vn-client-notification", "component": "vn-client-notification",
"description": "Notifications" "description": "Notifications"
}, { },
{
"url": "/unpaid", "url": "/unpaid",
"state": "client.card.unpaid", "state": "client.card.unpaid",
"component": "vn-client-unpaid", "component": "vn-client-unpaid",
@ -428,6 +430,13 @@
"state": "client.extendedList", "state": "client.extendedList",
"component": "vn-client-extended-list", "component": "vn-client-extended-list",
"description": "Extended list" "description": "Extended list"
},
{
"url": "/credit-management",
"state": "client.card.creditManagement",
"component": "vn-client-credit-management",
"acl": ["financial"],
"description": "Credit management"
} }
] ]
} }

View File

@ -253,7 +253,12 @@
</vn-label-value> </vn-label-value>
</vn-one> </vn-one>
<vn-one> <vn-one>
<h4 translate>Financial information</h4> <h4 ng-show="$ctrl.isEmployee">
<a target="_blank"
href="https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk={{::$ctrl.client.id}}">
<span translate vn-tooltip="Go to grafana">Financial information</span>
</a>
</h4>
<vn-label-value label="Risk" <vn-label-value label="Risk"
value="{{$ctrl.summary.debt.debt | currency: 'EUR':2}}" value="{{$ctrl.summary.debt.debt | currency: 'EUR':2}}"
ng-class="{alert: $ctrl.summary.debt.debt > $ctrl.summary.credit}" ng-class="{alert: $ctrl.summary.debt.debt > $ctrl.summary.credit}"
@ -282,6 +287,10 @@
ng-if="$ctrl.summary.recovery.started" ng-if="$ctrl.summary.recovery.started"
value="{{$ctrl.summary.recovery.started | date:'dd/MM/yyyy'}}"> value="{{$ctrl.summary.recovery.started | date:'dd/MM/yyyy'}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Rating"
value="{{$ctrl.summary.rating}}"
info="Value from 1 to 20. The higher the better value">
</vn-label-value>
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>

View File

@ -20,3 +20,6 @@ Invoices minus payments: Facturas menos recibos
Deviated invoices minus payments: Facturas fuera de plazo menos recibos Deviated invoices minus payments: Facturas fuera de plazo menos recibos
Go to the client: Ir al cliente Go to the client: Ir al cliente
Latest tickets: Últimos tickets Latest tickets: Últimos tickets
Rating: Clasificación
Value from 1 to 20. The higher the better value: Valor del 1 al 20. Cuanto más alto mejor valoración
Go to grafana: Ir a grafana