diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index b6629a50f..07eaf23fd 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -2434,4 +2434,12 @@ INSERT INTO `vn`.`expeditionScan` (`id`, `expeditionFk`, `scanned`, `palletFk`)
CALL `cache`.`last_buy_refresh`(FALSE);
UPDATE `vn`.`item` SET `genericFk` = 9
- WHERE `id` = 2;
\ No newline at end of file
+ WHERE `id` = 2;
+
+INSERT INTO `bs`.`defaulter` (`clientFk`, `amount`, `created`, `defaulterSinced`)
+ VALUES
+ (1101, 500, CURDATE(), CURDATE()),
+ (1102, 500, CURDATE(), CURDATE()),
+ (1103, 500, CURDATE(), CURDATE()),
+ (1107, 500, CURDATE(), CURDATE()),
+ (1109, 500, CURDATE(), CURDATE());
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index bd4078564..f0a5c37b5 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -304,6 +304,16 @@ export default {
saveNewInsuranceCredit: 'vn-client-credit-insurance-insurance-create button[type="submit"]',
anyCreditInsuranceLine: 'vn-client-credit-insurance-insurance-index vn-tbody > vn-tr',
},
+ clientDefaulter: {
+ anyClient: 'vn-client-defaulter-index vn-tbody > vn-tr',
+ firstClientName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(2) > span',
+ firstSalesPersonName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span',
+ firstObservation: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > vn-textarea[ng-model="defaulter.observation"]',
+ allDefaulterCheckbox: 'vn-client-defaulter-index vn-thead vn-multi-check',
+ addObservationButton: 'vn-client-defaulter-index vn-button[icon="icon-notes"]',
+ observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
+ saveButton: 'button[response="accept"]'
+ },
clientContacts: {
addContactButton: 'vn-client-contact vn-icon[icon="add_circle"]',
name: 'vn-client-contact vn-textfield[ng-model="contact.name"]',
diff --git a/e2e/paths/02-client/21_defaulter.spec.js b/e2e/paths/02-client/21_defaulter.spec.js
new file mode 100644
index 000000000..89b5c5761
--- /dev/null
+++ b/e2e/paths/02-client/21_defaulter.spec.js
@@ -0,0 +1,73 @@
+import selectors from '../../helpers/selectors.js';
+import getBrowser from '../../helpers/puppeteer';
+
+describe('Client defaulter path', () => {
+ let browser;
+ let page;
+
+ beforeAll(async() => {
+ browser = await getBrowser();
+ page = browser.page;
+ await page.loginAndModule('insurance', 'client');
+ await page.accessToSection('client.defaulter.index');
+ });
+
+ afterAll(async() => {
+ await browser.close();
+ });
+
+ it('should count the amount of clients in the turns section', async() => {
+ const result = await page.countElement(selectors.clientDefaulter.anyClient);
+
+ expect(result).toEqual(5);
+ });
+
+ it('should check contain expected client', async() => {
+ const clientName =
+ await page.waitToGetProperty(selectors.clientDefaulter.firstClientName, 'innerText');
+ const salesPersonName =
+ await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
+
+ expect(clientName).toEqual('Ororo Munroe');
+ expect(salesPersonName).toEqual('salesPerson');
+ });
+
+ it('should first observation not changed', async() => {
+ const expectedObservation = 'Madness, as you know, is like gravity, all it takes is a little push';
+ const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
+
+ expect(result).toContain(expectedObservation);
+ });
+
+ it('should not add empty observation', async() => {
+ await page.waitToClick(selectors.clientDefaulter.allDefaulterCheckbox);
+
+ await page.waitToClick(selectors.clientDefaulter.addObservationButton);
+ await page.write(selectors.clientDefaulter.observation, '');
+ await page.waitToClick(selectors.clientDefaulter.saveButton);
+ const message = await page.waitForSnackbar();
+
+ expect(message.text).toContain(`The message can't be empty`);
+ });
+
+ it('shoul checked all defaulters', async() => {
+ await page.loginAndModule('insurance', 'client');
+ await page.accessToSection('client.defaulter.index');
+
+ await page.waitToClick(selectors.clientDefaulter.allDefaulterCheckbox);
+ });
+
+ it('should add observation for all clients', async() => {
+ await page.waitToClick(selectors.clientDefaulter.addObservationButton);
+ await page.write(selectors.clientDefaulter.observation, 'My new observation');
+ await page.waitToClick(selectors.clientDefaulter.saveButton);
+ });
+
+ it('should first observation changed', async() => {
+ const message = await page.waitForSnackbar();
+ const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
+
+ expect(message.text).toContain('Observation saved!');
+ expect(result).toContain('My new observation');
+ });
+});
diff --git a/modules/client/back/methods/defaulter/filter.js b/modules/client/back/methods/defaulter/filter.js
new file mode 100644
index 000000000..c06d1c51b
--- /dev/null
+++ b/modules/client/back/methods/defaulter/filter.js
@@ -0,0 +1,90 @@
+
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+const buildFilter = require('vn-loopback/util/filter').buildFilter;
+const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
+
+module.exports = Self => {
+ Self.remoteMethodCtx('filter', {
+ description: 'Find all instances of the model matched by filter from the data source.',
+ accessType: 'READ',
+ accepts: [
+ {
+ arg: 'filter',
+ type: 'object',
+ description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
+ http: {source: 'query'}
+ },
+ {
+ arg: 'search',
+ type: 'string',
+ description: `If it's and integer searchs by id, otherwise it searchs by name`
+ }
+ ],
+ returns: {
+ type: ['object'],
+ root: true
+ },
+ http: {
+ path: `/filter`,
+ verb: 'GET'
+ }
+ });
+
+ Self.filter = async(ctx, filter, options) => {
+ const conn = Self.dataSource.connector;
+ const myOptions = {};
+
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const where = buildFilter(ctx.args, (param, value) => {
+ switch (param) {
+ case 'search':
+ return {or: [
+ {'d.clientFk': value},
+ {'d.clientName': {like: `%${value}%`}}
+ ]};
+ }
+ });
+
+ filter = mergeFilters(ctx.args.filter, {where});
+
+ const stmts = [];
+
+ const stmt = new ParameterizedSQL(
+ `SELECT *
+ FROM (
+ SELECT
+ DISTINCT c.id clientFk,
+ c.name clientName,
+ c.salesPersonFk,
+ u.name salesPersonName,
+ d.amount,
+ co.created,
+ CONCAT(DATE(co.created), ' ', co.text) observation,
+ uw.id workerFk,
+ uw.name workerName,
+ c.creditInsurance,
+ d.defaulterSinced
+ FROM vn.defaulter d
+ JOIN vn.client c ON c.id = d.clientFk
+ LEFT JOIN vn.clientObservation co ON co.clientFk = c.id
+ LEFT JOIN account.user u ON u.id = c.salesPersonFk
+ LEFT JOIN account.user uw ON uw.id = co.workerFk
+ WHERE
+ d.created = CURDATE()
+ AND d.amount > 0
+ ORDER BY co.created DESC) d`
+ );
+
+ stmt.merge(conn.makeWhere(filter.where));
+ stmt.merge(`GROUP BY d.clientFk`);
+ stmt.merge(conn.makeOrderBy(filter.order));
+
+ const itemsIndex = stmts.push(stmt) - 1;
+ const sql = ParameterizedSQL.join(stmts, ';');
+ const result = await conn.executeStmt(sql, myOptions);
+
+ return itemsIndex === 0 ? result : result[itemsIndex];
+ };
+};
diff --git a/modules/client/back/methods/defaulter/specs/filter.spec.js b/modules/client/back/methods/defaulter/specs/filter.spec.js
new file mode 100644
index 000000000..145bb5132
--- /dev/null
+++ b/modules/client/back/methods/defaulter/specs/filter.spec.js
@@ -0,0 +1,63 @@
+const models = require('vn-loopback/server/server').models;
+
+describe('defaulter filter()', () => {
+ const authUserId = 9;
+ it('should all return the tickets matching the filter', async() => {
+ const tx = await models.Defaulter.beginTransaction({});
+
+ try {
+ const options = {transaction: tx};
+ const filter = {};
+ const ctx = {req: {accessToken: {userId: authUserId}}, args: {filter: filter}};
+
+ const result = await models.Defaulter.filter(ctx, null, options);
+ const firstRow = result[0];
+
+ expect(firstRow.clientFk).toEqual(1101);
+ expect(result.length).toEqual(5);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+
+ it('should return the defaulter with id', async() => {
+ const tx = await models.Defaulter.beginTransaction({});
+
+ try {
+ const options = {transaction: tx};
+ const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 1101}};
+
+ const result = await models.Defaulter.filter(ctx, null, options);
+ const firstRow = result[0];
+
+ expect(firstRow.clientFk).toEqual(1101);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+
+ it('should return the defaulter matching the client name', async() => {
+ const tx = await models.Defaulter.beginTransaction({});
+
+ try {
+ const options = {transaction: tx};
+ const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'bruce'}};
+
+ const result = await models.Defaulter.filter(ctx, null, options);
+ const firstRow = result[0];
+
+ expect(firstRow.clientName).toEqual('Bruce Wayne');
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+});
diff --git a/modules/client/back/models/defaulter.js b/modules/client/back/models/defaulter.js
new file mode 100644
index 000000000..13bb1a614
--- /dev/null
+++ b/modules/client/back/models/defaulter.js
@@ -0,0 +1,3 @@
+module.exports = Self => {
+ require('../methods/defaulter/filter')(Self);
+};
diff --git a/modules/client/back/models/defaulter.json b/modules/client/back/models/defaulter.json
index 8d50356f1..829326435 100644
--- a/modules/client/back/models/defaulter.json
+++ b/modules/client/back/models/defaulter.json
@@ -8,6 +8,9 @@
}
},
"properties": {
+ "id": {
+ "type": "Number"
+ },
"created": {
"type": "Date"
},
diff --git a/modules/client/front/defaulter/index.html b/modules/client/front/defaulter/index.html
new file mode 100644
index 000000000..121556df2
--- /dev/null
+++ b/modules/client/front/defaulter/index.html
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Client
+ Comercial
+
+ Balance D.
+
+
+ Author
+
+ Last observation
+
+ Credit I.
+
+ From
+
+
+
+
+
+
+
+
+
+
+ {{::defaulter.clientName}}
+
+
+
+
+ {{::defaulter.salesPersonName | dashIfEmpty}}
+
+
+ {{::defaulter.amount}}
+
+
+ {{::defaulter.workerName | dashIfEmpty}}
+
+
+
+
+
+
+ {{::defaulter.creditInsurance}}
+ {{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Filter by selection
+
+
+ Exclude selection
+
+
+ Remove filter
+
+
+ Remove all filters
+
+
+ Copy value
+
+
+
+
+
+
+
+
+ {{$ctrl.$t('Add observation to all selected clients', {total: $ctrl.checked.length})}}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/client/front/defaulter/index.js b/modules/client/front/defaulter/index.js
new file mode 100644
index 000000000..76afeb160
--- /dev/null
+++ b/modules/client/front/defaulter/index.js
@@ -0,0 +1,65 @@
+import ngModule from '../module';
+import Section from 'salix/components/section';
+import UserError from 'core/lib/user-error';
+
+export default class Controller extends Section {
+ constructor($element, $) {
+ super($element, $);
+ this.defaulter = {};
+ }
+
+ get balanceDueTotal() {
+ let balanceDueTotal = 0;
+
+ if (this.checked.length > 0) {
+ for (let defaulter of this.checked)
+ balanceDueTotal += defaulter.amount;
+
+ return balanceDueTotal;
+ }
+
+ return balanceDueTotal;
+ }
+
+ get checked() {
+ const clients = this.$.model.data || [];
+ const checkedLines = [];
+ for (let defaulter of clients) {
+ if (defaulter.checked)
+ checkedLines.push(defaulter);
+ }
+
+ return checkedLines;
+ }
+
+ onResponse() {
+ if (!this.defaulter.observation)
+ throw new UserError(`The message can't be empty`);
+
+ const params = [];
+ for (let defaulter of this.checked) {
+ params.push({
+ text: this.defaulter.observation,
+ clientFk: defaulter.clientFk
+ });
+ }
+
+ this.$http.post(`ClientObservations`, params) .then(() => {
+ this.vnApp.showMessage(this.$t('Observation saved!'));
+ this.$state.reload();
+ });
+ }
+
+ exprBuilder(param, value) {
+ switch (param) {
+ case 'clientName':
+ case 'salesPersonFk':
+ return {[`d.${param}`]: value};
+ }
+ }
+}
+
+ngModule.vnComponent('vnClientDefaulterIndex', {
+ template: require('./index.html'),
+ controller: Controller
+});
diff --git a/modules/client/front/defaulter/index.spec.js b/modules/client/front/defaulter/index.spec.js
new file mode 100644
index 000000000..6428952ec
--- /dev/null
+++ b/modules/client/front/defaulter/index.spec.js
@@ -0,0 +1,98 @@
+import './index';
+import crudModel from 'core/mocks/crud-model';
+
+describe('client defaulter', () => {
+ describe('Component vnClientDefaulterIndex', () => {
+ let controller;
+ let $httpBackend;
+
+ beforeEach(ngModule('client'));
+
+ beforeEach(inject(($componentController, _$httpBackend_) => {
+ $httpBackend = _$httpBackend_;
+ const $element = angular.element('');
+ controller = $componentController('vnClientDefaulterIndex', {$element});
+ controller.$.model = crudModel;
+ controller.$.model.data = [
+ {clientFk: 1101, amount: 125},
+ {clientFk: 1102, amount: 500},
+ {clientFk: 1103, amount: 250}
+ ];
+ }));
+
+ describe('checked() getter', () => {
+ it('should return the checked lines', () => {
+ const data = controller.$.model.data;
+ data[1].checked = true;
+ data[2].checked = true;
+
+ const checkedRows = controller.checked;
+
+ const firstCheckedRow = checkedRows[0];
+ const secondCheckedRow = checkedRows[1];
+
+ expect(firstCheckedRow.clientFk).toEqual(1102);
+ expect(secondCheckedRow.clientFk).toEqual(1103);
+ });
+ });
+
+ describe('balanceDueTotal() getter', () => {
+ it('should return balance due total', () => {
+ const data = controller.$.model.data;
+ data[1].checked = true;
+ data[2].checked = true;
+
+ const checkedRows = controller.checked;
+ const expectedAmount = checkedRows[0].amount + checkedRows[1].amount;
+
+ const result = controller.balanceDueTotal;
+
+ expect(result).toEqual(expectedAmount);
+ });
+ });
+
+ describe('onResponse()', () => {
+ it('should return error for empty message', () => {
+ let error;
+ try {
+ controller.onResponse();
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBeDefined();
+ expect(error.message).toBe(`The message can't be empty`);
+ });
+
+ it('should return saved message', () => {
+ const data = controller.$.model.data;
+ data[1].checked = true;
+ controller.defaulter = {observation: 'My new observation'};
+
+ const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}];
+
+ jest.spyOn(controller.vnApp, 'showMessage');
+ $httpBackend.expect('POST', `ClientObservations`, params).respond(200, params);
+
+ controller.onResponse();
+ $httpBackend.flush();
+
+ expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Observation saved!');
+ });
+ });
+
+ describe('exprBuilder()', () => {
+ it('should search by sales person', () => {
+ let expr = controller.exprBuilder('salesPersonFk', '5');
+
+ expect(expr).toEqual({'d.salesPersonFk': '5'});
+ });
+
+ it('should search by client name', () => {
+ let expr = controller.exprBuilder('clientName', '1foo');
+
+ expect(expr).toEqual({'d.clientName': '1foo'});
+ });
+ });
+ });
+});
diff --git a/modules/client/front/defaulter/locale/es.yml b/modules/client/front/defaulter/locale/es.yml
new file mode 100644
index 000000000..172a3125d
--- /dev/null
+++ b/modules/client/front/defaulter/locale/es.yml
@@ -0,0 +1,7 @@
+Last observation: Última observación
+Add observation: Añadir observación
+Search client: Buscar clientes
+Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)
+Credit I.: Crédito A.
+Balance D.: Saldo V.
+Worker who made the last observation: Trabajador que ha realizado la última observación
\ No newline at end of file
diff --git a/modules/client/front/index.js b/modules/client/front/index.js
index 758b94e3f..6b35d392a 100644
--- a/modules/client/front/index.js
+++ b/modules/client/front/index.js
@@ -44,3 +44,4 @@ import './dms/create';
import './dms/edit';
import './consumption';
import './consumption-search-panel';
+import './defaulter';
diff --git a/modules/client/front/locale/es.yml b/modules/client/front/locale/es.yml
index 1a5a570a7..107931377 100644
--- a/modules/client/front/locale/es.yml
+++ b/modules/client/front/locale/es.yml
@@ -33,6 +33,7 @@ Search client by id or name: Buscar clientes por identificador o nombre
# Sections
Clients: Clientes
+Defaulter: Morosos
New client: Nuevo cliente
Fiscal data: Datos fiscales
Billing data: Forma de pago
diff --git a/modules/client/front/routes.json b/modules/client/front/routes.json
index 765fbc637..31a699e55 100644
--- a/modules/client/front/routes.json
+++ b/modules/client/front/routes.json
@@ -6,7 +6,8 @@
"dependencies": ["worker", "invoiceOut"],
"menus": {
"main": [
- {"state": "client.index", "icon": "person"}
+ {"state": "client.index", "icon": "person"},
+ {"state": "client.defaulter.index", "icon": "person"}
],
"card": [
{"state": "client.card.basicData", "icon": "settings"},
@@ -360,6 +361,18 @@
"params": {
"client": "$ctrl.client"
}
+ },
+ {
+ "url": "/defaulter",
+ "state": "client.defaulter",
+ "component": "ui-view",
+ "description": "Defaulter"
+ },
+ {
+ "url": "/index?q",
+ "state": "client.defaulter.index",
+ "component": "vn-client-defaulter-index",
+ "description": "Defaulter"
}
]
}