Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5475-email_2fa
This commit is contained in:
commit
641fdf39d8
|
@ -8,13 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [2314.01] - 2023-04-20
|
||||
|
||||
### Added
|
||||
- (Clientes -> Morosos) Ahora se puede filtrar por las columnas "Desde" y "Fecha Ú. O.". También se envia un email al comercial cuando se añade una nota.
|
||||
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
|
||||
- (Facturas recibidas -> Bases negativas) Nueva sección
|
||||
|
||||
### Changed
|
||||
-
|
||||
|
||||
### Fixed
|
||||
-
|
||||
- (Clientes -> Morosos) Ahora se mantienen los elementos seleccionados al hacer sroll.
|
||||
|
||||
## [2312.01] - 2023-04-06
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId)
|
||||
VALUES('Mail', '*', '*', 'ALLOW', 'ROLE', 'employee');
|
|
@ -268,9 +268,11 @@
|
|||
"Exists an invoice with a previous date": "Existe una factura con fecha anterior",
|
||||
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite",
|
||||
"Warehouse inventory not set": "El almacén inventario no está establecido",
|
||||
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
|
||||
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
|
||||
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
|
||||
"Not exist this branch": "La rama no existe",
|
||||
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
|
||||
"Insert a date range": "Inserte un rango de fechas"
|
||||
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
|
||||
"Insert a date range": "Inserte un rango de fechas",
|
||||
"Added observation": "{{user}} añadió esta observacion: {{text}}",
|
||||
"Comment added to client": "Observación añadida al cliente {{clientFk}}"
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('checkDuplicatedData', {
|
||||
description: 'Checks if a client has same email, mobile or phone than other client and send an email',
|
||||
accepts: [{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The client id'
|
||||
}],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
verb: 'GET',
|
||||
path: '/:id/checkDuplicatedData'
|
||||
}
|
||||
});
|
||||
|
||||
Self.checkDuplicatedData = async function(id, options) {
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
const client = await Self.app.models.Client.findById(id, myOptions);
|
||||
|
||||
const findParams = [];
|
||||
if (client.email) {
|
||||
const emails = client.email.split(',');
|
||||
for (let email of emails)
|
||||
findParams.push({email: email});
|
||||
}
|
||||
|
||||
if (client.phone)
|
||||
findParams.push({phone: client.phone});
|
||||
|
||||
if (client.mobile)
|
||||
findParams.push({mobile: client.mobile});
|
||||
|
||||
const filterObj = {
|
||||
where: {
|
||||
and: [
|
||||
{or: findParams},
|
||||
{id: {neq: client.id}}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const clientSameData = await Self.findOne(filterObj, myOptions);
|
||||
|
||||
if (clientSameData) {
|
||||
await Self.app.models.Mail.create({
|
||||
receiver: 'direccioncomercial@verdnatura.es',
|
||||
subject: `Cliente con email/teléfono/móvil duplicados`,
|
||||
body: 'El cliente ' + client.id + ' comparte alguno de estos datos con el cliente ' + clientSameData.id +
|
||||
'\n- Email: ' + client.email +
|
||||
'\n- Teléfono: ' + client.phone +
|
||||
'\n- Móvil: ' + client.mobile
|
||||
}, myOptions);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,24 +0,0 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
|
||||
describe('client checkDuplicated()', () => {
|
||||
it('should send an mail if mobile/phone/email is duplicated', async() => {
|
||||
const tx = await models.Client.beginTransaction({});
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
const id = 1110;
|
||||
const mailModel = models.Mail;
|
||||
spyOn(mailModel, 'create');
|
||||
|
||||
await models.Client.checkDuplicatedData(id, options);
|
||||
|
||||
expect(mailModel.create).toHaveBeenCalled();
|
||||
|
||||
await tx.rollback();
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('observationEmail', {
|
||||
description: 'Send an email with the observation',
|
||||
accessType: 'WRITE',
|
||||
accepts: [
|
||||
{
|
||||
arg: 'defaulters',
|
||||
type: ['object'],
|
||||
required: true,
|
||||
description: 'The defaulters to send the email'
|
||||
},
|
||||
{
|
||||
arg: 'observation',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The observation'
|
||||
}],
|
||||
returns: {
|
||||
arg: 'observationEmail'
|
||||
},
|
||||
http: {
|
||||
path: `/observationEmail`,
|
||||
verb: 'POST'
|
||||
}
|
||||
});
|
||||
|
||||
Self.observationEmail = async(ctx, defaulters, observation, options) => {
|
||||
const models = Self.app.models;
|
||||
const $t = ctx.req.__; // $translate
|
||||
const myOptions = {};
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
for (const defaulter of defaulters) {
|
||||
const user = await models.Account.findById(userId, {fields: ['name']}, myOptions);
|
||||
|
||||
const body = $t('Added observation', {
|
||||
user: user.name,
|
||||
text: observation
|
||||
});
|
||||
|
||||
await models.Mail.create({
|
||||
subject: $t('Comment added to client', {clientFk: defaulter.clientFk}),
|
||||
body: body,
|
||||
receiver: `${defaulter.salesPersonName}@verdnatura.es`,
|
||||
replyTo: `${user.name}@verdnatura.es`
|
||||
}, myOptions);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -2,7 +2,6 @@ module.exports = Self => {
|
|||
require('../methods/client/addressesPropagateRe')(Self);
|
||||
require('../methods/client/canBeInvoiced')(Self);
|
||||
require('../methods/client/canCreateTicket')(Self);
|
||||
require('../methods/client/checkDuplicated')(Self);
|
||||
require('../methods/client/confirmTransaction')(Self);
|
||||
require('../methods/client/consumption')(Self);
|
||||
require('../methods/client/createAddress')(Self);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/defaulter/filter')(Self);
|
||||
require('../methods/defaulter/observationEmail')(Self);
|
||||
};
|
||||
|
|
|
@ -9,9 +9,7 @@ export default class Controller extends Section {
|
|||
}
|
||||
|
||||
onSubmit() {
|
||||
return this.$.watcher.submit().then(() => {
|
||||
this.$http.get(`Clients/${this.$params.id}/checkDuplicatedData`);
|
||||
});
|
||||
return this.$.watcher.submit();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ export default class Controller extends Section {
|
|||
onSubmit() {
|
||||
return this.$.watcher.submit().then(json => {
|
||||
this.$state.go('client.card.basicData', {id: json.data.id});
|
||||
this.$http.get(`Clients/${this.client.id}/checkDuplicatedData`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
limit="20"
|
||||
order="amount DESC"
|
||||
data="defaulters"
|
||||
on-data-change="$ctrl.reCheck()"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<vn-portal slot="topbar">
|
||||
|
@ -17,22 +18,22 @@
|
|||
</vn-searchbar>
|
||||
</vn-portal>
|
||||
<vn-card>
|
||||
<smart-table
|
||||
<smart-table
|
||||
model="model"
|
||||
options="$ctrl.smartTableOptions"
|
||||
options="$ctrl.smartTableOptions"
|
||||
expr-builder="$ctrl.exprBuilder(param, value)">
|
||||
<slot-actions>
|
||||
<div>
|
||||
<div class="totalBox" style="text-align: center;">
|
||||
<h6 translate>Total</h6>
|
||||
<vn-label-value
|
||||
label="Balance due"
|
||||
<vn-label-value
|
||||
label="Balance due"
|
||||
value="{{$ctrl.balanceDueTotal | currency: 'EUR': 2}}">
|
||||
</vn-label-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vn-pa-md">
|
||||
<vn-button
|
||||
<vn-button
|
||||
ng-show="$ctrl.checked.length > 0"
|
||||
ng-click="notesDialog.show()"
|
||||
name="notesDialog"
|
||||
|
@ -46,7 +47,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th shrink>
|
||||
<vn-multi-check
|
||||
<vn-multi-check
|
||||
model="model">
|
||||
</vn-multi-check>
|
||||
</th>
|
||||
|
@ -56,25 +57,25 @@
|
|||
<th field="salesPersonFk">
|
||||
<span translate>Comercial</span>
|
||||
</th>
|
||||
<th
|
||||
field="amount"
|
||||
<th
|
||||
field="amount"
|
||||
vn-tooltip="Balance due">
|
||||
<span translate>Balance D.</span>
|
||||
</th>
|
||||
<th
|
||||
field="workerFk"
|
||||
<th
|
||||
field="workerFk"
|
||||
vn-tooltip="Worker who made the last observation">
|
||||
<span translate>Author</span>
|
||||
</th>
|
||||
<th field="observation" expand>
|
||||
<span translate>Last observation</span>
|
||||
</th>
|
||||
<th
|
||||
<th
|
||||
vn-tooltip="Last observation date"
|
||||
field="created">
|
||||
<span translate>L. O. Date</span>
|
||||
</th>
|
||||
<th
|
||||
<th
|
||||
vn-tooltip="Credit insurance"
|
||||
field="creditInsurance"
|
||||
shrink>
|
||||
|
@ -88,8 +89,9 @@
|
|||
<tbody>
|
||||
<tr ng-repeat="defaulter in defaulters">
|
||||
<td shrink>
|
||||
<vn-check
|
||||
<vn-check
|
||||
ng-model="defaulter.checked"
|
||||
on-change="$ctrl.saveChecked(defaulter.clientFk)"
|
||||
vn-click-stop>
|
||||
</vn-check>
|
||||
</td>
|
||||
|
@ -150,7 +152,7 @@
|
|||
</vn-client-summary>
|
||||
</vn-popup>
|
||||
|
||||
<!-- Dialog of add notes button -->
|
||||
<!-- Dialog of add notes button -->
|
||||
<vn-dialog
|
||||
vn-id="notesDialog"
|
||||
on-accept="$ctrl.onResponse()">
|
||||
|
@ -160,7 +162,7 @@
|
|||
<vn-horizontal>
|
||||
<vn-textarea vn-one
|
||||
vn-id="message"
|
||||
label="Message"
|
||||
label="Message"
|
||||
ng-model="$ctrl.defaulter.observation"
|
||||
rows="3"
|
||||
required="true"
|
||||
|
|
|
@ -6,6 +6,7 @@ export default class Controller extends Section {
|
|||
constructor($element, $) {
|
||||
super($element, $);
|
||||
this.defaulter = {};
|
||||
this.checkedDefaulers = [];
|
||||
|
||||
this.smartTableOptions = {
|
||||
activeButtons: {
|
||||
|
@ -45,11 +46,11 @@ export default class Controller extends Section {
|
|||
},
|
||||
{
|
||||
field: 'created',
|
||||
searchable: false
|
||||
datepicker: true
|
||||
},
|
||||
{
|
||||
field: 'defaulterSinced',
|
||||
searchable: false
|
||||
datepicker: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -68,6 +69,19 @@ export default class Controller extends Section {
|
|||
return checkedLines;
|
||||
}
|
||||
|
||||
saveChecked(clientId) {
|
||||
this.checkedDefaulers = this.checkedDefaulers.includes(clientId) ?
|
||||
this.checkedDefaulers.filter(id => id !== clientId) : [...this.checkedDefaulers, clientId];
|
||||
}
|
||||
|
||||
reCheck() {
|
||||
if (!this.$.model.data || !this.checkedDefaulers.length) return;
|
||||
|
||||
this.$.model.data.forEach(defaulter => {
|
||||
defaulter.checked = this.checkedDefaulers.includes(defaulter.clientFk);
|
||||
});
|
||||
}
|
||||
|
||||
getBalanceDueTotal() {
|
||||
this.$http.get('Defaulters/filter')
|
||||
.then(res => {
|
||||
|
@ -109,11 +123,20 @@ export default class Controller extends Section {
|
|||
}
|
||||
|
||||
this.$http.post(`ClientObservations`, params) .then(() => {
|
||||
this.vnApp.showMessage(this.$t('Observation saved!'));
|
||||
this.vnApp.showSuccess(this.$t('Observation saved!'));
|
||||
this.sendMail();
|
||||
this.$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
sendMail() {
|
||||
const params = {
|
||||
defaulters: this.checked,
|
||||
observation: this.defaulter.observation
|
||||
};
|
||||
this.$http.post(`Defaulters/observationEmail`, params);
|
||||
}
|
||||
|
||||
exprBuilder(param, value) {
|
||||
switch (param) {
|
||||
case 'creditInsurance':
|
||||
|
@ -122,8 +145,25 @@ export default class Controller extends Section {
|
|||
case 'workerFk':
|
||||
case 'salesPersonFk':
|
||||
return {[`d.${param}`]: value};
|
||||
case 'created':
|
||||
return {'d.created': {
|
||||
between: this.dateRange(value)}
|
||||
};
|
||||
case 'defaulterSinced':
|
||||
return {'d.defaulterSinced': {
|
||||
between: this.dateRange(value)}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
dateRange(value) {
|
||||
const minHour = new Date(value);
|
||||
minHour.setHours(0, 0, 0, 0);
|
||||
const maxHour = new Date(value);
|
||||
maxHour.setHours(23, 59, 59, 59);
|
||||
|
||||
return [minHour, maxHour];
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnClientDefaulter', {
|
||||
|
|
|
@ -81,14 +81,15 @@ describe('client defaulter', () => {
|
|||
|
||||
const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}];
|
||||
|
||||
jest.spyOn(controller.vnApp, 'showMessage');
|
||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||
$httpBackend.expect('GET', `Defaulters/filter`).respond(200);
|
||||
$httpBackend.expect('POST', `ClientObservations`, params).respond(200, params);
|
||||
$httpBackend.expect('POST', `Defaulters/observationEmail`).respond(200);
|
||||
|
||||
controller.onResponse();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Observation saved!');
|
||||
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Observation saved!');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,5 +118,62 @@ describe('client defaulter', () => {
|
|||
expect(controller.balanceDueTotal).toEqual(875);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dateRange()', () => {
|
||||
it('should return two dates with the hours at the start and end of the given date', () => {
|
||||
const now = Date.vnNew();
|
||||
|
||||
const today = now.getDate();
|
||||
|
||||
const dateRange = controller.dateRange(now);
|
||||
const start = dateRange[0].toString();
|
||||
const end = dateRange[1].toString();
|
||||
|
||||
expect(start).toContain(today);
|
||||
expect(start).toContain('00:00:00');
|
||||
|
||||
expect(end).toContain(today);
|
||||
expect(end).toContain('23:59:59');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reCheck()', () => {
|
||||
it(`should recheck buys`, () => {
|
||||
controller.$.model.data = [
|
||||
{checked: false, clientFk: 1},
|
||||
{checked: false, clientFk: 2},
|
||||
{checked: false, clientFk: 3},
|
||||
{checked: false, clientFk: 4},
|
||||
];
|
||||
controller.checkedDefaulers = [1, 2];
|
||||
|
||||
controller.reCheck();
|
||||
|
||||
expect(controller.$.model.data[0].checked).toEqual(true);
|
||||
expect(controller.$.model.data[1].checked).toEqual(true);
|
||||
expect(controller.$.model.data[2].checked).toEqual(false);
|
||||
expect(controller.$.model.data[3].checked).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveChecked()', () => {
|
||||
it(`should check buy`, () => {
|
||||
const buyCheck = 3;
|
||||
controller.checkedDefaulers = [1, 2];
|
||||
|
||||
controller.saveChecked(buyCheck);
|
||||
|
||||
expect(controller.checkedDefaulers[2]).toEqual(buyCheck);
|
||||
});
|
||||
|
||||
it(`should uncheck buy`, () => {
|
||||
const buyUncheck = 3;
|
||||
controller.checkedDefaulers = [1, 2, 3];
|
||||
|
||||
controller.saveChecked(buyUncheck);
|
||||
|
||||
expect(controller.checkedDefaulers[2]).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,4 +6,6 @@ Last observation: Última observación
|
|||
L. O. Date: Fecha Ú. O.
|
||||
Last observation date: Fecha última observación
|
||||
Search client: Buscar clientes
|
||||
Worker who made the last observation: Trabajador que ha realizado la última observación
|
||||
Worker who made the last observation: Trabajador que ha realizado la última observación
|
||||
Email sended!: Email enviado!
|
||||
Observation saved!: Observación añadida!
|
||||
|
|
|
@ -38,6 +38,19 @@
|
|||
url="Warehouses">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="vn-px-lg">
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.requesterFk"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Comercial">
|
||||
<tpl-item>{{firstName}} {{lastName}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
||||
<section class="vn-px-md">
|
||||
<vn-horizontal class="manifold-panel vn-pa-md">
|
||||
<vn-date-picker
|
||||
|
|
|
@ -5,4 +5,5 @@ M3 Price: Precio M3
|
|||
Route Price: Precio ruta
|
||||
Minimum Km: Km minimos
|
||||
Remove row: Eliminar fila
|
||||
Add row: Añadir fila
|
||||
Add row: Añadir fila
|
||||
New autonomous: Nuevo autónomo
|
||||
|
|
|
@ -37,6 +37,10 @@ module.exports = Self => {
|
|||
type: 'number',
|
||||
description: `Search requests attended by a given worker id`
|
||||
},
|
||||
{
|
||||
arg: 'requesterFk',
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
arg: 'mine',
|
||||
type: 'boolean',
|
||||
|
@ -89,6 +93,8 @@ module.exports = Self => {
|
|||
return {'t.id': value};
|
||||
case 'attenderFk':
|
||||
return {'tr.attenderFk': value};
|
||||
case 'requesterFk':
|
||||
return {'tr.requesterFk': value};
|
||||
case 'state':
|
||||
switch (value) {
|
||||
case 'pending':
|
||||
|
@ -125,6 +131,7 @@ module.exports = Self => {
|
|||
tr.description,
|
||||
tr.response,
|
||||
tr.saleFk,
|
||||
tr.requesterFk,
|
||||
tr.isOk,
|
||||
s.quantity AS saleQuantity,
|
||||
s.itemFk,
|
||||
|
|
Loading…
Reference in New Issue