diff --git a/db/changes/10410-january/00-smsConfig.sql b/db/changes/10410-january/00-smsConfig.sql deleted file mode 100644 index b3f1610d4..000000000 --- a/db/changes/10410-january/00-smsConfig.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE `vn`.`smsConfig` ADD apiKey varchar(50) NULL; -ALTER TABLE `vn`.`smsConfig` CHANGE `user` user__ varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL; -ALTER TABLE `vn`.`smsConfig` CHANGE password password__ varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL; -ALTER TABLE `vn`.`sms` MODIFY COLUMN statusCode smallint(9) DEFAULT 0 NULL; -ALTER TABLE `vn`.`sms` MODIFY COLUMN status varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT 'OK' NULL; diff --git a/db/changes/10410-january/01-smsConfig_update.sql b/db/changes/10410-january/01-smsConfig_update.sql deleted file mode 100644 index c4b4c895e..000000000 --- a/db/changes/10410-january/01-smsConfig_update.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE `vn`.`smsConfig` - SET `uri` = 'https://api.gateway360.com/api/3.0/sms/send' - WHERE `id` = 1; - -UPDATE `vn`.`smsConfig` - SET `apiKey` = '5715476da95b46d686a5a255e6459523' - WHERE `id` = 1; \ No newline at end of file diff --git a/db/changes/10410-january/00-acl.sql b/db/changes/10411-january/00-acl.sql similarity index 100% rename from db/changes/10410-january/00-acl.sql rename to db/changes/10411-january/00-acl.sql diff --git a/db/changes/10410-january/00-booking.sql b/db/changes/10411-january/00-booking.sql similarity index 100% rename from db/changes/10410-january/00-booking.sql rename to db/changes/10411-january/00-booking.sql diff --git a/db/changes/10411-january/00-companyGroup.sql b/db/changes/10411-january/00-companyGroup.sql new file mode 100644 index 000000000..2c5477e63 --- /dev/null +++ b/db/changes/10411-january/00-companyGroup.sql @@ -0,0 +1,12 @@ +UPDATE `vn`.`companyGroup` + SET `code`='verdnatura' + WHERE `id`=1; +UPDATE `vn`.`companyGroup` + SET `code`='ornamental' + WHERE `id`=2; +UPDATE `vn`.`companyGroup` + SET `code`='other' + WHERE `id`=3; +UPDATE `vn`.`companyGroup` + SET `code`='provisional' + WHERE `id`=4; diff --git a/db/changes/10410-january/00-saleVolume.sql b/db/changes/10411-january/00-saleVolume.sql similarity index 100% rename from db/changes/10410-january/00-saleVolume.sql rename to db/changes/10411-january/00-saleVolume.sql diff --git a/db/changes/10411-january/00-ticket_getMovable.sql b/db/changes/10411-january/00-ticket_getMovable.sql new file mode 100644 index 000000000..5f5b0a93a --- /dev/null +++ b/db/changes/10411-january/00-ticket_getMovable.sql @@ -0,0 +1,43 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticket_getMovable`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`ticket_getMovable`(vTicketFk INT, vDatedNew DATETIME, vWarehouseFk INT) +BEGIN +/** + * Cálcula el stock movible para los artículos de un ticket + * + * @param vTicketFk -> Ticket + * @param vDatedNew -> Nueva fecha + * @return Sales con Movible +*/ + DECLARE vDatedOld DATETIME; + + SELECT t.shipped INTO vDatedOld + FROM ticket t + WHERE t.id = vTicketFk; + + CALL itemStock(vWarehouseFk, DATE_SUB(vDatedNew, INTERVAL 1 DAY), NULL); + CALL item_getMinacum(vWarehouseFk, vDatedNew, DATEDIFF(vDatedOld, vDatedNew), NULL); + + SELECT s.id, + s.itemFk, + s.quantity, + s.concept, + s.price, + s.reserved, + s.discount, + i.image, + i.subName, + il.stock + IFNULL(im.amount, 0) AS movable + FROM ticket t + JOIN sale s ON s.ticketFk = t.id + JOIN item i ON i.id = s.itemFk + LEFT JOIN tmp.itemMinacum im ON im.itemFk = s.itemFk AND im.warehouseFk = vWarehouseFk + LEFT JOIN tmp.itemList il ON il.itemFk = s.itemFk + WHERE t.id = vTicketFk; + + DROP TEMPORARY TABLE IF EXISTS tmp.itemList; + DROP TEMPORARY TABLE IF EXISTS tmp.itemMinacum; +END$$ +DELIMITER ; diff --git a/db/changes/10410-january/00-workerLabour.sql b/db/changes/10411-january/00-workerLabour.sql similarity index 100% rename from db/changes/10410-january/00-workerLabour.sql rename to db/changes/10411-january/00-workerLabour.sql diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index efe7906ce..07eaf23fd 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -456,7 +456,7 @@ INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `crea INSERT INTO `vn`.`companyGroup`(`id`, `code`) VALUES (1, 'wayneIndustries'), - (2, 'Verdnatura'); + (2, 'verdnatura'); INSERT INTO `vn`.`bankEntity`(`id`, `countryFk`, `name`, `bic`) VALUES @@ -625,6 +625,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF (25 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, CURDATE()), (26 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, CURDATE()), (27 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, CURDATE()); + INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`) VALUES (1, 11, 1, 'ready'), @@ -899,7 +900,8 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric (29, 4, 17, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()), (31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, CURDATE()), - (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()); + (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()), + (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, CURDATE()); INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`) VALUES @@ -2432,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 2900a285b..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"]', @@ -526,6 +536,7 @@ export default { acceptDialog: '.vn-dialog.shown button[response="accept"]', acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]', descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(4) > section > span', + descriptorDeliveryAgency: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(5) > section > span', acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]', acceptDeleteStowawayButton: '.vn-dialog.shown button[response="accept"]' }, @@ -603,10 +614,12 @@ export default { ticketBasicData: { agency: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]', zone: 'vn-autocomplete[ng-model="$ctrl.zoneId"]', + shipped: 'vn-date-picker[ng-model="$ctrl.shipped"]', nextStepButton: 'vn-step-control .buttons > section:last-child vn-button', finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]', stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > vn-side-menu div:nth-child(4)', chargesReason: 'vn-ticket-basic-data-step-two div:nth-child(3) > vn-radio', + withoutNegatives: 'vn-check[ng-model="$ctrl.ticket.withoutNegatives"]', }, ticketComponents: { base: 'vn-ticket-components > vn-side-menu div:nth-child(1) > div:nth-child(2)' 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/e2e/paths/05-ticket/06_basic_data_steps.spec.js b/e2e/paths/05-ticket/06_basic_data_steps.spec.js index a5f9a60cf..7a09edf06 100644 --- a/e2e/paths/05-ticket/06_basic_data_steps.spec.js +++ b/e2e/paths/05-ticket/06_basic_data_steps.spec.js @@ -83,4 +83,62 @@ describe('Ticket Edit basic data path', () => { await page.waitToClick(selectors.ticketBasicData.finalizeButton); await page.waitForState('ticket.card.summary'); }); + + it(`should not find ticket`, async() => { + await page.doSearch('29'); + const count = await page.countElement(selectors.ticketsIndex.searchResult); + + expect(count).toEqual(0); + }); + + it(`should split ticket without negatives`, async() => { + const newAgency = 'Silla247'; + const newDate = new Date(); + newDate.setDate(newDate.getDate() + 1); + + await page.accessToSearchResult('14'); + await page.accessToSection('ticket.card.basicData.stepOne'); + + await page.autocompleteSearch(selectors.ticketBasicData.agency, newAgency); + await page.pickDate(selectors.ticketBasicData.shipped, newDate); + + await page.waitToClick(selectors.ticketBasicData.nextStepButton); + + await page.waitToClick(selectors.ticketBasicData.withoutNegatives); + await page.waitToClick(selectors.ticketBasicData.finalizeButton); + + await page.waitForState('ticket.card.summary'); + + const newTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const newTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(newAgency).toEqual(newTicketAgency); + expect(newTicketDate).toContain(newDate.getDate()); + }); + + it(`should new ticket have sale of old ticket`, async() => { + await page.accessToSection('ticket.card.sale'); + await page.waitForState('ticket.card.sale'); + + const item = await page.waitToGetProperty(selectors.ticketSales.firstSaleId, 'innerText'); + + expect(item).toEqual('4'); + }); + + it(`should old ticket have old date and agency`, async() => { + const oldDate = new Date(); + const oldAgency = 'Super-Man delivery'; + + await page.accessToSearchResult('14'); + + const oldTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const oldTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(oldTicketAgency).toEqual(oldAgency); + expect(oldTicketDate).toContain(oldDate.getDate()); + }); }); 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 @@ + + + + + + + + + +
+
+
Total
+ + +
+
+
+ + +
+
+ + + + + + + + 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" } ] } diff --git a/modules/item/front/waste/index/style.scss b/modules/item/front/waste/index/style.scss index 09e3a2a4c..faac46139 100644 --- a/modules/item/front/waste/index/style.scss +++ b/modules/item/front/waste/index/style.scss @@ -11,7 +11,7 @@ vn-item-waste-detail { padding-bottom: 7px; padding-bottom: 4px; font-weight: lighter; - background-color: #fde6ca; + background-color: $color-bg; border-bottom: 1px solid #f7931e; white-space: nowrap; overflow: hidden; diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index 53e5fedc8..de06212c7 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -77,6 +77,12 @@ module.exports = Self => { type: 'number', description: 'Action id', required: true + }, + { + arg: 'isWithoutNegatives', + type: 'boolean', + description: 'Is whithout negatives', + required: true }], returns: { type: ['object'], @@ -127,6 +133,18 @@ module.exports = Self => { } } + if (args.isWithoutNegatives) { + const query = `CALL ticket_getMovable(?,?,?)`; + const params = [args.id, args.shipped, args.warehouseFk]; + const [salesMovable] = await Self.rawSql(query, params, myOptions); + + const salesNewTicket = salesMovable.filter(sale => (sale.movable ?? 0) >= sale.quantity); + if (salesNewTicket.length) { + const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions); + args.id = newTicket.id; + } + } + const originalTicket = await models.Ticket.findOne({ where: {id: args.id}, fields: [ @@ -230,8 +248,9 @@ module.exports = Self => { await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); } + res.id = args.id; if (tx) await tx.commit(); - + return res; } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/ticket/back/methods/ticket/priceDifference.js b/modules/ticket/back/methods/ticket/priceDifference.js index 1856cb08f..c91956ece 100644 --- a/modules/ticket/back/methods/ticket/priceDifference.js +++ b/modules/ticket/back/methods/ticket/priceDifference.js @@ -40,6 +40,12 @@ module.exports = Self => { type: 'number', description: 'The warehouse id', required: true + }, + { + arg: 'shipped', + type: 'date', + description: 'shipped', + required: true }], returns: { type: ['object'], @@ -104,19 +110,32 @@ module.exports = Self => { totalDifference: 0.00, }; - const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`; - const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId]; + // Get items movable + const ticketOrigin = await models.Ticket.findById(args.id, null, myOptions); + const differenceShipped = ticketOrigin.shipped.getTime() != args.shipped.getTime(); + const differenceWarehouse = ticketOrigin.warehouseFk != args.warehouseId; + + salesObj.haveDifferences = differenceShipped || differenceWarehouse; + + let query = `CALL ticket_getMovable(?,?,?)`; + let params = [args.id, args.shipped, args.warehouseId]; + const [salesMovable] = await Self.rawSql(query, params, myOptions); + + const itemMovable = new Map(); + for (sale of salesMovable) + itemMovable.set(sale.id, sale.movable ?? 0); + + // Sale price component, one per sale + query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`; + params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId]; const [difComponents] = await Self.rawSql(query, params, myOptions); const map = new Map(); - - // Sale price component, one per sale for (difComponent of difComponents) map.set(difComponent.saleFk, difComponent); for (sale of salesObj.items) { const difComponent = map.get(sale.id); - if (difComponent) { sale.component = difComponent; @@ -129,10 +148,11 @@ module.exports = Self => { salesObj.totalUnitPrice += sale.price; salesObj.totalUnitPrice = round(salesObj.totalUnitPrice); + sale.movable = itemMovable.get(sale.id); } if (tx) await tx.commit(); - + return salesObj; } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js index 9fa69b595..2aa2a07c4 100644 --- a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js +++ b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js @@ -45,7 +45,8 @@ describe('ticket componentUpdate()', () => { shipped: today, landed: tomorrow, isDeleted: false, - option: 1 + option: 1, + isWithoutNegatives: false }; let ctx = { @@ -94,7 +95,8 @@ describe('ticket componentUpdate()', () => { shipped: today, landed: tomorrow, isDeleted: false, - option: 1 + option: 1, + isWithoutNegatives: false }; const ctx = { @@ -134,4 +136,60 @@ describe('ticket componentUpdate()', () => { throw e; } }); + + it('should change warehouse and without negatives', async() => { + const tx = await models.SaleComponent.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const saleToTransfer = 27; + const originDate = today; + const newDate = tomorrow; + const ticketID = 14; + newDate.setHours(0, 0, 0, 0, 0); + originDate.setHours(0, 0, 0, 0, 0); + + const args = { + id: ticketID, + clientFk: 1104, + agencyModeFk: 2, + addressFk: 4, + zoneFk: 9, + warehouseFk: 1, + companyFk: 442, + shipped: newDate, + landed: tomorrow, + isDeleted: false, + option: 1, + isWithoutNegatives: true + }; + + const ctx = { + args: args, + req: { + accessToken: {userId: 9}, + headers: {origin: 'http://localhost'}, + __: value => { + return value; + } + } + }; + await models.Ticket.componentUpdate(ctx, options); + + const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options); + const oldTicket = await models.Ticket.findById(ticketID, null, options); + const newTicket = await models.Ticket.findById(newTicketID.id, null, options); + const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options); + + expect(oldTicket.shipped).toEqual(originDate); + expect(newTicket.shipped).toEqual(newDate); + expect(newTicketSale.id).toEqual(saleToTransfer); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js index fed899d77..e9aa5030a 100644 --- a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js +++ b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js @@ -15,6 +15,7 @@ describe('sale priceDifference()', () => { ctx.args = { id: 16, landed: tomorrow, + shipped: tomorrow, addressId: 126, agencyModeId: 7, zoneId: 3, @@ -45,6 +46,7 @@ describe('sale priceDifference()', () => { ctx.args = { id: 1, landed: new Date(), + shipped: new Date(), addressId: 121, zoneId: 3, warehouseId: 1 @@ -59,4 +61,38 @@ describe('sale priceDifference()', () => { expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`)); }); + + it('should return ticket movable', async() => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const ctx = {req: {accessToken: {userId: 1106}}}; + ctx.args = { + id: 11, + shipped: tomorrow, + landed: tomorrow, + addressId: 122, + agencyModeId: 7, + zoneId: 3, + warehouseId: 1 + }; + + const result = await models.Ticket.priceDifference(ctx, options); + const firstItem = result.items[0]; + const secondtItem = result.items[1]; + + expect(firstItem.movable).toEqual(440); + expect(secondtItem.movable).toEqual(1980); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/ticket/front/basic-data/step-one/index.js b/modules/ticket/front/basic-data/step-one/index.js index 215f36a46..f532265e2 100644 --- a/modules/ticket/front/basic-data/step-one/index.js +++ b/modules/ticket/front/basic-data/step-one/index.js @@ -201,7 +201,8 @@ class Controller extends Component { addressId: this.ticket.addressFk, agencyModeId: this.ticket.agencyModeFk, zoneId: this.ticket.zoneFk, - warehouseId: this.ticket.warehouseFk + warehouseId: this.ticket.warehouseFk, + shipped: this.ticket.shipped }; return this.$http.post(query, params).then(res => { diff --git a/modules/ticket/front/basic-data/step-two/index.html b/modules/ticket/front/basic-data/step-two/index.html index 439f2b527..092c9e746 100644 --- a/modules/ticket/front/basic-data/step-two/index.html +++ b/modules/ticket/front/basic-data/step-two/index.html @@ -9,6 +9,7 @@ Item Description + Movable Quantity Price (PPU) New (PPU) @@ -31,6 +32,13 @@ tabindex="-1"> + + + {{::sale.movable}} + + {{::sale.quantity}} {{::sale.price | currency: 'EUR': 2}} {{::sale.component.newPrice | currency: 'EUR': 2}} @@ -66,6 +74,13 @@ +
+ + +
diff --git a/modules/ticket/front/basic-data/step-two/index.js b/modules/ticket/front/basic-data/step-two/index.js index 7ffdb8315..c12647aa5 100644 --- a/modules/ticket/front/basic-data/step-two/index.js +++ b/modules/ticket/front/basic-data/step-two/index.js @@ -20,6 +20,7 @@ class Controller extends Component { this.getTotalNewPrice(); this.getTotalDifferenceOfPrice(); this.loadDefaultTicketAction(); + this.ticketHaveNegatives(); } loadDefaultTicketAction() { @@ -63,6 +64,22 @@ class Controller extends Component { this.totalPriceDifference = totalPriceDifference; } + ticketHaveNegatives() { + let haveNegatives = false; + let haveNotNegatives = false; + const haveDifferences = this.ticket.sale.haveDifferences; + + this.ticket.sale.items.forEach(item => { + if (item.quantity > item.movable) + haveNegatives = true; + else + haveNotNegatives = true; + }); + + this.ticket.withoutNegatives = false; + this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences); + } + onSubmit() { if (!this.ticket.option) { return this.vnApp.showError( @@ -70,8 +87,8 @@ class Controller extends Component { ); } - let query = `tickets/${this.ticket.id}/componentUpdate`; - let params = { + const query = `tickets/${this.ticket.id}/componentUpdate`; + const params = { clientFk: this.ticket.clientFk, nickname: this.ticket.nickname, agencyModeFk: this.ticket.agencyModeFk, @@ -82,16 +99,20 @@ class Controller extends Component { shipped: this.ticket.shipped, landed: this.ticket.landed, isDeleted: this.ticket.isDeleted, - option: parseInt(this.ticket.option) + option: parseInt(this.ticket.option), + isWithoutNegatives: this.ticket.withoutNegatives }; - this.$http.post(query, params).then(res => { - this.vnApp.showMessage( - this.$t(`The ticket has been unrouted`) - ); - this.card.reload(); - this.$state.go('ticket.card.summary', {id: this.$params.id}); - }); + this.$http.post(query, params) + .then(res => { + this.ticketToMove = res.data.id; + this.vnApp.showMessage( + this.$t(`The ticket has been unrouted`) + ); + }) + .finally(() => { + this.$state.go('ticket.card.summary', {id: this.ticketToMove}); + }); } } diff --git a/modules/ticket/front/basic-data/step-two/index.spec.js b/modules/ticket/front/basic-data/step-two/index.spec.js index ea8268716..ac85a7818 100644 --- a/modules/ticket/front/basic-data/step-two/index.spec.js +++ b/modules/ticket/front/basic-data/step-two/index.spec.js @@ -64,5 +64,103 @@ describe('Ticket', () => { expect(controller.totalPriceDifference).toEqual(0.3); }); }); + + describe('ticketHaveNegatives()', () => { + it('should show if ticket have any negative, have differences, but not all sale are negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 1, + movable: 5 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(true); + }); + + it('should not show if ticket not have any negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 2, + movable: 1 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + + it('should not show if all sale are negative', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 2, + movable: 1 + } + ], + haveDifferences: true + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + + it('should not show if ticket not have differences', () => { + controller.ticket = { + sale: { + items: [ + { + item: 1, + quantity: 2, + movable: 1 + }, + { + item: 2, + quantity: 1, + movable: 2 + } + ], + haveDifferences: false + } + }; + + controller.ticketHaveNegatives(); + + expect(controller.haveNegatives).toEqual(false); + }); + }); }); }); diff --git a/modules/ticket/front/basic-data/step-two/locale/es.yml b/modules/ticket/front/basic-data/step-two/locale/es.yml index a2a07991b..e1f1e0bfc 100644 --- a/modules/ticket/front/basic-data/step-two/locale/es.yml +++ b/modules/ticket/front/basic-data/step-two/locale/es.yml @@ -5,4 +5,7 @@ Charge difference to: Cargar diferencia a The ticket has been unrouted: El ticket ha sido desenrutado Price: Precio New price: Nuevo precio -Price difference: Diferencia de precio \ No newline at end of file +Price difference: Diferencia de precio +Create without negatives: Crear sin negativos +Clone this ticket with the changes and only sales availables: Clona este ticket con los cambios y solo las ventas disponibles. +Movable: Movible \ No newline at end of file