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

This commit is contained in:
Joan Sanchez 2022-02-01 10:57:39 +00:00
commit a8900b06b7
33 changed files with 1036 additions and 38 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 ;

View File

@ -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;
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());

View File

@ -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)'

View File

@ -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');
});
});

View File

@ -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());
});
});

View File

@ -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];
};
};

View File

@ -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;
}
});
});

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/defaulter/filter')(Self);
};

View File

@ -8,6 +8,9 @@
}
},
"properties": {
"id": {
"type": "Number"
},
"created": {
"type": "Date"
},

View File

@ -0,0 +1,186 @@
<vn-crud-model
vn-id="model"
url="Defaulters/filter"
filter="::$ctrl.filter"
limit="20"
data="defaulters"
auto-load="true">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
placeholder="Search client"
info="Search client by id or name"
auto-state="false"
model="model">
</vn-searchbar>
</vn-portal>
<vn-data-viewer
model="model"
class="vn-w-xl">
<vn-card>
<vn-tool-bar>
<div class="vn-pa-md">
<div class="totalBox" style="text-align: center;">
<h6 translate>Total</h6>
<vn-label-value
label="Balance due"
value="{{$ctrl.balanceDueTotal}} €">
</vn-label-value>
</div>
</div>
<div class="vn-pa-md">
<vn-button
ng-show="$ctrl.checked.length > 0"
ng-click="notesDialog.show()"
name="notesDialog"
vn-tooltip="Add observation"
icon="icon-notes">
</vn-button>
</div>
</vn-tool-bar>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="clientName">Client</vn-th>
<vn-th field="salesPersonFk">Comercial</vn-th>
<vn-th
field="amount"
vn-tooltip="Balance due"
number>
Balance D.
</vn-th>
<vn-th
vn-tooltip="Worker who made the last observation"
shrink>
Author
</vn-th>
<vn-th expand>Last observation</vn-th>
<vn-th
vn-tooltip="Credit insurance"
number>
Credit I.
</vn-th>
<vn-th shrink-datetime>From</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="defaulter in defaulters">
<vn-td shrink>
<vn-check
ng-model="defaulter.checked"
vn-click-stop>
</vn-check>
</vn-td>
<vn-td>
<span
vn-click-stop="clientDescriptor.show($event, defaulter.clientFk)"
title ="{{::defaulter.clientName}}"
class="link">
{{::defaulter.clientName}}
</span>
</vn-td>
<vn-td>
<span
title="{{::defaulter.salesPersonName}}"
vn-click-stop="workerDescriptor.show($event, defaulter.salesPersonFk)"
class="link" >
{{::defaulter.salesPersonName | dashIfEmpty}}
</span>
</vn-td>
<vn-td number>{{::defaulter.amount}}</vn-td>
<vn-td shrink>
<span
title="{{::defaulter.workerName}}"
vn-click-stop="workerDescriptor.show($event, defaulter.workerFk)"
class="link" >
{{::defaulter.workerName | dashIfEmpty}}
</span>
</vn-td>
<vn-td expand>
<vn-textarea
vn-three
disabled="true"
label="Observation"
ng-model="defaulter.observation">
</vn-textarea>
</vn-td>
<vn-td number>{{::defaulter.creditInsurance}}</vn-td>
<vn-td shrink-datetime>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="dialog-summary-client">
<vn-client-summary
client="$ctrl.clientSelected">
</vn-client-summary>
</vn-popup>
<!--Context menu-->
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>
<!-- Dialog of add notes button -->
<vn-dialog
vn-id="notesDialog"
on-accept="$ctrl.onResponse()">
<tpl-body>
<section class="SMSDialog">
<h5 class="vn-py-sm">{{$ctrl.$t('Add observation to all selected clients', {total: $ctrl.checked.length})}}</h5>
<vn-horizontal>
<vn-textarea vn-one
vn-id="message"
label="Message"
ng-model="$ctrl.defaulter.observation"
rows="3"
required="true"
rule>
</vn-textarea>
</vn-horizontal>
</section>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -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
});

View File

@ -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('<vn-client-defaulter></vn-client-defaulter>');
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'});
});
});
});
});

View File

@ -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

View File

@ -44,3 +44,4 @@ import './dms/create';
import './dms/edit';
import './consumption';
import './consumption-search-panel';
import './defaulter';

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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;
}
});
});

View File

@ -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;
}
});
});

View File

@ -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 => {

View File

@ -9,6 +9,7 @@
<vn-tr>
<vn-th number>Item</vn-th>
<vn-th class="align-center">Description</vn-th>
<vn-th ng-if="$ctrl.ticket.sale.haveDifferences" number>Movable</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price (PPU)</vn-th>
<vn-th number>New (PPU)</vn-th>
@ -31,6 +32,13 @@
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td ng-if="$ctrl.ticket.sale.haveDifferences" number>
<span
class="chip"
ng-class="{'alert': sale.quantity>sale.movable}">
{{::sale.movable}}
</span>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.price | currency: 'EUR': 2}}</vn-td>
<vn-td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</vn-td>
@ -66,6 +74,13 @@
</div>
</div>
</vn-card>
<div class="totalBox align-left" ng-show="::$ctrl.haveNegatives">
<vn-check
ng-model="$ctrl.ticket.withoutNegatives"
label="Create without negatives"
info="Clone this ticket with the changes and only sales availables">
</vn-check>
</div>
</div>
</vn-side-menu>

View File

@ -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});
});
}
}

View File

@ -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);
});
});
});
});

View File

@ -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
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