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

This commit is contained in:
Joan Sanchez 2021-04-27 08:30:59 +00:00
commit 68b81f176e
55 changed files with 643 additions and 344 deletions

View File

@ -8,7 +8,9 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
* Node.js = 14.15.1 LTS
* Node.js = 14.x LTS
* Docker
* Git
* Docker
You will need to install globally the following items.

View File

@ -0,0 +1,2 @@
ALTER TABLE vn.`supplierAccount` ADD `beneficiary` VARCHAR(50) NULL DEFAULT NULL AFTER `bankFk`;
UPDATE vn.supplierAccount SET beneficiary = `description`;

View File

@ -1,24 +1,28 @@
drop procedure weekWaste;
DROP PROCEDURE IF EXISTS `bs`.`weekWaste`;
create definer = root@`%` procedure weekWaste__()
DELIMITER $$
$$
CREATE DEFINER = `root`@`%` PROCEDURE `bs`.`weekWaste__`()
BEGIN
DECLARE vWeek INT;
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = DATE_ADD(CURDATE(), INTERVAL -1 WEEK);
INTO vWeek, vYear
FROM vn.time
WHERE dated = DATE_ADD(CURDATE(), INTERVAL -1 WEEK);
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
sum(saleTotal) as total,
sum(saleWaste) as dwindle
FROM bs.waste
WHERE year = vYear and week = vWeek
GROUP BY buyer
) sub
ORDER BY percentage DESC;
END;
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
SUM(saleTotal) AS total,
SUM(saleWaste) AS dwindle
FROM bs.waste
WHERE year = vYear
AND week = vWeek
GROUP BY buyer
) sub
ORDER BY percentage DESC;
END;$$
DELIMITER ;

View File

@ -1,28 +1,32 @@
drop procedure weekWaste_byWorker;
DROP PROCEDURE IF EXISTS `bs`.`weekWaste_byWorker`;
create definer = root@`%` procedure weekWaste_byWorker__(IN vWorkerFk int)
DELIMITER $$
$$
CREATE
DEFINER = root@`%` PROCEDURE `bs`.`weekWaste_byWorker__`(IN vWorkerFk INT)
BEGIN
DECLARE vWeek INT;
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = TIMESTAMPADD(WEEK,-1,CURDATE());
INTO vWeek, vYear
FROM vn.time
WHERE dated = TIMESTAMPADD(WEEK, -1, CURDATE());
SELECT *, 100 * mermas / total as porcentaje
FROM (
SELECT ws.family,
sum(ws.saleTotal) as total,
sum(ws.saleWaste) as mermas
FROM bs.waste ws
SELECT *, 100 * mermas / total AS porcentaje
FROM (
SELECT ws.family,
SUM(ws.saleTotal) AS total,
SUM(ws.saleWaste) AS mermas
FROM bs.waste ws
JOIN vn.worker w ON w.user = ws.buyer
WHERE year = vYear AND week = vWeek
AND w.id = vWorkerFk
GROUP BY family
) sub
ORDER BY porcentaje DESC;
END;
WHERE year = vYear
AND week = vWeek
AND w.id = vWorkerFk
GROUP BY family
) sub
ORDER BY porcentaje DESC;
END;;$$
DELIMITER ;

View File

@ -1,25 +1,30 @@
drop procedure weekWaste_getDetail;
DROP PROCEDURE IF EXISTS `bs`.`weekWaste_getDetail`;
create definer = root@`%` procedure weekWaste_getDetail__()
DELIMITER $$
$$
CREATE
DEFINER = root@`%` PROCEDURE `bs`.`weekWaste_getDetail__`()
BEGIN
DECLARE vLastWeek DATE;
DECLARE vWeek INT;
DECLARE vLastWeek DATE;
DECLARE vWeek INT;
DECLARE vYear INT;
SET vLastWeek = TIMESTAMPADD(WEEK,-1,CURDATE());
SET vLastWeek = TIMESTAMPADD(WEEK, -1, CURDATE());
SET vYear = YEAR(vLastWeek);
SET vWeek = WEEK(vLastWeek, 1);
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE year = vYear AND week = vWeek
GROUP BY buyer, family
) sub
ORDER BY percentage DESC;
END;
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
SUM(ws.saleTotal) AS total,
SUM(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE year = vYear
AND week = vWeek
GROUP BY buyer, family
) sub
ORDER BY percentage DESC;
END;$$
DELIMITER ;

View File

@ -17,6 +17,5 @@ ALTER TABLE `bs`.`waste`
ALTER TABLE `bs`.`waste` DROP PRIMARY KEY;
ALTER TABLE `bs`.`waste`
AD PRIMARY KEY (buyer, year, week, family, itemFk);
ADD PRIMARY KEY (buyer, `year`, week, family, itemFk);

View File

@ -2,27 +2,28 @@ UPDATE `bs`.nightTask t SET t.`procedure` = 'waste_addSales' WHERE t.id = 54;
DROP PROCEDURE IF EXISTS `bs`.`waste_Add`;
DELIMITER $$
$$
CREATE
DEFINER = root@`%` PROCEDURE `bs`.`waste_addSales`()
DEFINER = root@`%` PROCEDURE `bs`.`waste_addSales`()
BEGIN
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = CURDATE();
FROM vn.time
WHERE dated = CURDATE();
REPLACE bs.waste
REPLACE bs.waste
SELECT *, 100 * mermas / total as porcentaje
FROM (
SELECT buyer,
year,
week,
week,
family,
itemFk,
itemTypeFk,
itemFk,
itemTypeFk,
floor(sum(value)) as total,
floor(sum(IF(clientTypeFk = 'loses', value, 0))) as mermas
FROM vn.saleValue
@ -32,7 +33,5 @@ BEGIN
) sub
ORDER BY mermas DESC;
END;
END;$$
DELIMITER ;

View File

@ -0,0 +1,3 @@
UPDATE salix.ACL
SET principalId = "salesAssistant"
WHERE model = 'Client' AND property = 'createReceipt';

View File

@ -904,6 +904,25 @@ export default {
ticketOne: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(1)',
ticketTwo: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(2)'
},
invoiceInSummary: {
supplierRef: 'vn-invoice-in-summary vn-label-value:nth-child(2) > section > span'
},
invoiceInDescriptor: {
moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]',
moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]',
acceptDeleteButton: '.vn-confirm.shown button[response="accept"]'
},
invoiceInBasicData: {
issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]',
operated: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.operated"]',
supplier: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.supplierFk"]',
supplierRef: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.supplierRef"]',
bookEntried: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.bookEntried"]',
booked: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.booked"]',
currency: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.currencyFk"]',
company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]',
save: 'vn-invoice-in-basic-data button[type=submit]'
},
travelIndex: {
anySearchResult: 'vn-travel-index vn-tbody > a',
firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)',

View File

@ -159,7 +159,6 @@ describe('Client Edit fiscalData path', () => {
});
it('should propagate the Equalization tax changes', async() => {
await page.waitForTimeout(1000);
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();

View File

@ -81,7 +81,6 @@ describe('Client Add address path', () => {
});
it(`should confirm the new address exists and it's the default one`, async() => {
await page.waitForTimeout(2000); // needs more than a single second to load the section
const result = await page.waitToGetProperty(selectors.clientAddresses.defaultAddress, 'innerText');
expect(result).toContain('320 Park Avenue New York');

View File

@ -52,7 +52,6 @@ describe('User config', () => {
it('should open the user config form to check the settings', async() => {
await page.waitToClick(selectors.globalItems.userMenuButton);
await page.waitForTimeout(1000);
let expectedLocalWarehouse = await page
.expectPropertyValue(selectors.globalItems.userLocalWarehouse, 'value', '');

View File

@ -28,7 +28,6 @@ describe('Client contacts', () => {
});
it('should delete de contact', async() => {
await page.waitForTimeout(3000);
await page.waitToClick(selectors.clientContacts.deleteFirstPhone);
await page.waitToClick(selectors.clientContacts.saveButton);
const message = await page.waitForSnackbar();

View File

@ -20,7 +20,6 @@ describe('Client credit insurance path', () => {
});
it('should open the create a new credit contract form', async() => {
await page.waitForTimeout(1000);
await page.waitToClick(selectors.clientCreditInsurance.addNewContract);
await page.waitForState('client.card.creditInsurance.create');
});

View File

@ -45,8 +45,7 @@ describe('Item Edit basic data path', () => {
await page.waitToClick(selectors.itemBasicData.newIntrastatButton);
await page.write(selectors.itemBasicData.newIntrastatId, '588420239');
await page.write(selectors.itemBasicData.newIntrastatDescription, 'Tropical Flowers');
await page.waitToClick(selectors.itemBasicData.acceptIntrastatButton); // this popover obscures the rest of the form for aprox 2 seconds
await page.waitForTimeout(2000);
await page.waitToClick(selectors.itemBasicData.acceptIntrastatButton);
await page.waitForTextInField(selectors.itemBasicData.intrastat, 'Tropical Flowers');
let newcode = await page.waitToGetProperty(selectors.itemBasicData.intrastat, 'value');

View File

@ -55,7 +55,6 @@ describe('Item index path', () => {
});
it('should mark all unchecked boxes to leave the index as it was', async() => {
await page.waitForTimeout(500); // otherwise the snackbar doesnt appear some times.
await page.waitToClick(selectors.itemsIndex.fieldsToShowButton);
await page.waitToClick(selectors.itemsIndex.idCheckbox);
await page.waitToClick(selectors.itemsIndex.stemsCheckbox);

View File

@ -45,7 +45,6 @@ describe('Item fixed prices path', () => {
await page.writeOnEditableTD(selectors.itemFixedPrice.fourthMinPrice, '5');
await page.pickDate(selectors.itemFixedPrice.fourthStarted, now);
await page.pickDate(selectors.itemFixedPrice.fourthEnded, now);
await page.waitForTimeout(1000);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');

View File

@ -143,7 +143,6 @@ describe('Ticket Edit sale path', () => {
});
it('should remove 1 from the first sale quantity', async() => {
await page.waitForTimeout(500);
await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell);
await page.waitForSelector(selectors.ticketSales.firstSaleQuantity);
await page.type(selectors.ticketSales.firstSaleQuantity, '9\u000d');
@ -225,16 +224,12 @@ describe('Ticket Edit sale path', () => {
it('should search for a ticket then access to the sales section', async() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitForTimeout(2000);
});
it('should select the third sale and delete it', async() => {
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitForTimeout(2000);
await page.waitToClick(selectors.ticketSales.deleteSaleButton);
await page.waitForTimeout(2000);
await page.waitToClick(selectors.globalItems.acceptButton);
await page.waitForTimeout(2000);
await page.waitForSpinnerLoad();
const message = await page.waitForSnackbar();

View File

@ -47,7 +47,6 @@ describe('Ticket Create new tracking state path', () => {
});
it(`should attemp to create an state for which salesPerson doesn't have permissions`, async() => {
await page.waitForTimeout(1500);
await page.autocompleteSearch(selectors.createStateView.state, 'Encajado');
await page.waitToClick(selectors.createStateView.saveStateButton);
const message = await page.waitForSnackbar();

View File

@ -51,7 +51,6 @@ describe('Ticket Edit basic data path', () => {
it(`should edit the ticket agency then check there are no zones for it`, async() => {
await page.autocompleteSearch(selectors.ticketBasicData.agency, 'Super-Man delivery');
await page.waitForTimeout(1000);
let emptyZone = await page
.expectPropertyValue(selectors.ticketBasicData.zone, 'value', '');

View File

@ -18,7 +18,6 @@ describe('Ticket purchase request path', () => {
});
it('should add a new request', async() => {
await page.waitForTimeout(500);
await page.waitToClick(selectors.ticketRequests.addRequestButton);
await page.write(selectors.ticketRequests.descriptionInput, 'New stuff');
await page.write(selectors.ticketRequests.quantity, '9');

View File

@ -184,7 +184,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('SMS sent!');
expect(message).toBeDefined();
});
it('should send the import SMS using the descriptor menu', async() => {
@ -196,7 +196,7 @@ describe('Ticket descriptor path', () => {
await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('SMS sent!');
expect(message).toBeDefined();
});
});
});

View File

@ -42,7 +42,6 @@ describe('Ticket create path', () => {
it('should again open the new ticket form', async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitForTimeout(500);
await page.waitToClick(selectors.ticketsIndex.newTicketButton);
await page.waitForState('ticket.create');
});

View File

@ -25,10 +25,6 @@ describe('Claim action path', () => {
});
it('should import the second importable ticket', async() => {
// the animation adding the header element for the claimed total
// obscures somehow other elements for about 2 seconds
await page.waitForTimeout(3000);
await page.waitToClick(selectors.claimAction.importTicketButton);
await page.waitToClick(selectors.claimAction.secondImportableTicket);
const message = await page.waitForSnackbar();

View File

@ -8,7 +8,6 @@ describe('Order summary path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.waitForTimeout(2000);
await page.accessToSearchResult('16');
});

View File

@ -17,7 +17,6 @@ describe('Route create path', () => {
describe('as employee', () => {
it('should click on the add new route button and open the creation form', async() => {
await page.waitForTimeout(500);
await page.waitToClick(selectors.routeIndex.addNewRouteButton);
await page.waitForState('route.create');
});
@ -74,7 +73,6 @@ describe('Route create path', () => {
});
it(`should clone the first route`, async() => {
await page.waitForTimeout(1000); // needs time for the index to show all items
await page.waitToClick(selectors.routeIndex.firstRouteCheckbox);
await page.waitToClick(selectors.routeIndex.cloneButton);
await page.waitToClick(selectors.routeIndex.submitClonationButton);

View File

@ -0,0 +1,28 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSearchResult('1');
});
afterAll(async() => {
await browser.close();
});
it('should reach the summary section', async() => {
await page.waitForState('invoiceIn.card.summary');
});
it('should contain some basic data from the invoice', async() => {
const result = await page.waitToGetProperty(selectors.invoiceInSummary.supplierRef, 'innerText');
expect(result).toEqual('1234');
});
});

View File

@ -0,0 +1,38 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn descriptor path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSearchResult('10');
});
afterAll(async() => {
await browser.close();
});
it('should delete the invoiceIn using the descriptor more menu', async() => {
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn);
await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('InvoiceIn deleted');
});
it('should have been relocated to the invoiceOut index', async() => {
await page.waitForState('invoiceIn.index');
});
it(`should search for the deleted invouceOut to find no results`, async() => {
await page.doSearch('10');
const nResults = await page.countElement(selectors.invoiceOutIndex.searchResult);
expect(nResults).toEqual(0);
});
});

View File

@ -0,0 +1,64 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn basic data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSearchResult('1');
await page.accessToSection('invoiceIn.card.basicData');
});
afterAll(async() => {
await browser.close();
});
it(`should edit the invoiceIn basic data`, async() => {
const now = new Date();
await page.pickDate(selectors.invoiceInBasicData.issued, now);
await page.pickDate(selectors.invoiceInBasicData.operated, now);
await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura');
await page.clearInput(selectors.invoiceInBasicData.supplierRef);
await page.write(selectors.invoiceInBasicData.supplierRef, '9999');
await page.pickDate(selectors.invoiceInBasicData.bookEntried, now);
await page.pickDate(selectors.invoiceInBasicData.booked, now);
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'Dollar USA');
await page.autocompleteSearch(selectors.invoiceInBasicData.company, 'ORN');
await page.waitToClick(selectors.invoiceInBasicData.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should confirm the invoiceIn supplier was edited`, async() => {
await page.reloadSection('invoiceIn.card.basicData');
const result = await page.waitToGetProperty(selectors.invoiceInBasicData.supplier, 'value');
expect(result).toContain('Verdnatura');
});
it(`should confirm the invoiceIn supplierRef was edited`, async() => {
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.supplierRef, 'value');
expect(result).toEqual('9999');
});
it(`should confirm the invoiceIn currency was edited`, async() => {
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.currency, 'value');
expect(result).toEqual('Dollar USA');
});
it(`should confirm the invoiceIn company was edited`, async() => {
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.company, 'value');
expect(result).toEqual('ORN');
});
});

View File

@ -44,7 +44,6 @@ describe('Travel basic data path', () => {
it('should now edit the whole form then save', async() => {
await page.clearInput(selectors.travelBasicData.reference);
await page.write(selectors.travelBasicData.reference, 'new reference!');
await page.waitForTimeout(2000);
await page.autocompleteSearch(selectors.travelBasicData.agency, 'Entanglement');
await page.autocompleteSearch(selectors.travelBasicData.outputWarehouse, 'Warehouse Three');
await page.autocompleteSearch(selectors.travelBasicData.inputWarehouse, 'Warehouse Four');

View File

@ -22,7 +22,6 @@ describe('Travel extra community path', () => {
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelReference, 'edited reference');
await page.waitForSpinnerLoad();
await page.writeOnEditableTD(selectors.travelExtraCommunity.firstTravelLockedKg, '1500');
await page.waitForTimeout(1000);
});
it('should reload the index and confirm the reference and locked kg were edited', async() => {

View File

@ -17,7 +17,6 @@ describe('Entry lastest buys path', () => {
it('should access the latest buys seccion and search not seeing the edit buys button yet', async() => {
await page.waitToClick(selectors.entryLatestBuys.latestBuysSectionButton);
await page.waitForTimeout(250);
await page.waitToClick(selectors.globalItems.searchButton);
await page.waitForSelector(selectors.entryLatestBuys.editBuysButton, {visible: false});
});

View File

@ -58,24 +58,21 @@
label="Save">
</vn-submit>
<vn-button
disabled="!watcher.dataChanged()"
label="Synchronize all"
ng-click="$ctrl.onSynchronizeAll()">
</vn-button>
<vn-button
disabled="!watcher.dataChanged()"
label="Synchronize user"
ng-click="syncUser.show()">
</vn-button>
<vn-button
disabled="!watcher.dataChanged()"
label="Synchronize roles"
ng-click="$ctrl.onSynchronizeRoles()">
</vn-button>
<vn-button
disabled="!watcher.dataChanged()"
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>

View File

@ -53,7 +53,6 @@
label="Save">
</vn-submit>
<vn-button
disabled="watcher.dataChanged()"
label="Test connection"
ng-click="$ctrl.onTestConection()">
</vn-button>

View File

@ -1,4 +1,5 @@
const app = require('vn-loopback/server/server');
const soap = require('soap');
describe('client sendSms()', () => {
let createdLog;
@ -9,7 +10,8 @@ describe('client sendSms()', () => {
done();
});
it('should send a message and log it', async() => {
it('should now send a message and log it', async() => {
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
let ctx = {req: {accessToken: {userId: 9}}};
let id = 101;
let destination = 222222222;

View File

@ -4,34 +4,11 @@ const soap = require('soap');
describe('sms send()', () => {
it('should return the expected message and status code', async() => {
const code = 200;
const smsConfig = await app.models.SmsConfig.findOne();
const soapClient = await soap.createClientAsync(smsConfig.uri);
spyOn(soap, 'createClientAsync').and.returnValue(soapClient);
spyOn(soapClient, 'sendSMSAsync').and.returnValue([{
result: {
$value:
`<xtratelecom-sms-response>
<sms>
<codigo>
${code}
</codigo>
<descripcion>
Envio en procesamiento
</descripcion>
<messageId>
1
</messageId>
</sms>
<procesoId>
444328681
</procesoId>
</xtratelecom-sms-response>`
}
}]);
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
let ctx = {req: {accessToken: {userId: 1}}};
let result = await app.models.Sms.send(ctx, 105, 'destination', 'My SMS Body');
expect(result.statusCode).toEqual(200);
expect(result.statusCode).toEqual(code);
expect(result.status).toContain('Fake response');
});
});

View File

@ -128,7 +128,7 @@
</vn-data-viewer>
</div>
<vn-float-button
vn-acl="administrative"
vn-acl="salesAssistant"
vn-acl-action="remove"
icon="add"
vn-tooltip="New payment"

View File

@ -13,7 +13,8 @@
on-error-src/>
</div>
<div class="description">
<h3>
<h3 class="link"
ng-click="itemDescriptor.show($event, item.id)">
{{::item.name}}
</h3>
<h4 class="ellipsize">
@ -65,3 +66,6 @@
vn-id="pricesPopover"
order="$ctrl.order">
</vn-order-prices-popover>
<vn-item-descriptor-popover
vn-id="itemDescriptor">
</vn-item-descriptor-popover>

View File

@ -1,75 +1,52 @@
<default>
<vn-descriptor-content
module="item"
description="$ctrl.item.name"
descriptor="$ctrl"
class="vn-order-prices-popover">
<slot-body>
<div class="attributes">
<vn-label-value
label="Buyer"
value="{{$ctrl.item.firstName}} {{$ctrl.item.lastName}}">
</vn-label-value>
<vn-label-value
ng-repeat="tag in $ctrl.tags"
label="{{::tag.tag.name}}"
value="{{::tag.value}}">
</vn-label-value>
</div>
<div class="quicklinks">
<vn-quick-link
tooltip="Diary"
state="['item.card.diary', {id: $ctrl.id}]"
icon="icon-transaction">
</vn-quick-link>
</div>
</slot-body>
<slot-after>
<form name="form" class="prices">
<vn-table>
<vn-tbody>
<vn-tr ng-repeat="price in $ctrl.prices">
<vn-td shrink class="warehouse">
<span
class="ellipsize text"
title="{{::price.warehouse}}">
{{::price.warehouse}}
</span>
</vn-td>
<vn-td number expand>
<div>
<span
ng-click="$ctrl.addQuantity(price)"
class="link unselectable">{{::price.grouping}}</span>
<span> x {{::price.price | currency: 'EUR': 2}}</span>
</div>
<div class="price-kg" ng-show="::price.priceKg">
{{price.priceKg | currency: 'EUR'}}/Kg
</div>
</vn-td>
<vn-td shrink>
<vn-input-number
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
on-change="$ctrl.validate()"
class="dense">
</vn-input-number>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div class="footer vn-pa-md">
<div class="error">
<span translate>Wrong quantity</span>
<form name="form" class="prices">
<vn-table>
<vn-tbody>
<vn-tr ng-repeat="price in $ctrl.prices">
<vn-td class="warehouse" expand>
<span
class="text"
title="{{::price.warehouse}}">
{{::price.warehouse}}
</span>
</vn-td>
<vn-td number expand>
<div>
<span
ng-click="$ctrl.addQuantity(price)"
class="link unselectable">{{::price.grouping}}</span>
<span> x {{::price.price | currency: 'EUR': 2}}</span>
</div>
<vn-submit
label="Save"
ng-click="$ctrl.submit()">
</vn-submit>
</div>
</form>
</slot-after>
</vn-descriptor-content>
<div class="price-kg" ng-show="::price.priceKg">
{{::price.priceKg | currency: 'EUR'}}/Kg
</div>
</vn-td>
<vn-td shrink>
<!-- Focus first element -->
<vn-input-number ng-if="$index === 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense"
vn-focus>
</vn-input-number>
<vn-input-number ng-if="$index > 0"
min="0"
name="quantity"
ng-model="price.quantity"
step="price.grouping"
class="dense">
</vn-input-number>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<div class="footer vn-pa-md">
<vn-submit
label="Add"
ng-click="$ctrl.submit()">
</vn-submit>
</div>
</form>
</default>

View File

@ -11,34 +11,18 @@ class Controller extends Popover {
set prices(value) {
this._prices = value;
if (value && value[0].grouping)
this.calculateTotal();
this.getTotalQuantity();
}
get prices() {
return this._prices;
}
getTags() {
const filter = {
where: {
itemFk: this.id,
priority: {gte: 4}
},
order: 'priority ASC',
include: {relation: 'tag'}
};
this.$http.get(`ItemTags`, {filter})
.then(res => {
this.tags = res.data;
this.$.$applyAsync(() => this.relocate());
});
}
show(parent, item) {
this.item = JSON.parse(JSON.stringify(item));
this.id = item.id;
this.prices = this.item.prices;
this.getTags();
super.show(parent);
}
@ -47,67 +31,74 @@ class Controller extends Popover {
this.item = {};
this.tags = {};
this._prices = {};
this.total = 0;
this.totalQuantity = 0;
super.onClose();
}
calculateMax() {
this.max = this.item.available - this.total;
}
calculateTotal() {
this.total = 0;
this.prices.forEach(price => {
getTotalQuantity() {
let total = 0;
for (let price of this.prices) {
if (!price.quantity) price.quantity = 0;
this.total += price.quantity;
});
this.calculateMax();
total += price.quantity;
}
this.totalQuantity = total;
this.maxQuantity = this.item.available - total;
}
addQuantity(price) {
if (this.total + price.grouping <= this.max)
this.getTotalQuantity();
const quantity = this.totalQuantity + price.grouping;
if (quantity <= this.maxQuantity)
price.quantity += price.grouping;
}
getFilledLines() {
const filledLines = [];
let match;
this.prices.forEach(price => {
getGroupings() {
const filledRows = [];
for (let price of this.prices) {
if (price.quantity && price.quantity > 0) {
match = filledLines.find(element => {
return element.warehouseFk == price.warehouseFk;
const priceMatch = filledRows.find(row => {
return row.warehouseFk == price.warehouseFk;
});
if (!match) {
filledLines.push(Object.assign({}, price));
return;
}
match.quantity += price.quantity;
if (!priceMatch)
filledRows.push(Object.assign({}, price));
else priceMatch.quantity += price.quantity;
}
});
return filledLines;
}
return filledRows;
}
submit() {
this.calculateTotal();
const filledLines = this.getFilledLines();
const filledRows = this.getGroupings();
if (filledLines.length <= 0) {
this.vnApp.showError(this.$t('First you must add some quantity'));
return;
try {
const hasValidGropings = filledRows.some(row =>
row.quantity % row.grouping == 0
);
if (filledRows.length <= 0)
throw new Error('First you must add some quantity');
if (!hasValidGropings)
throw new Error(`The amounts doesn't match with the grouping`);
const params = {
orderFk: this.order.id,
items: filledRows
};
this.$http.post(`OrderRows/addToOrder`, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.hide();
if (this.card) this.card.reload();
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
const params = {
orderFk: this.order.id,
items: filledLines
};
this.$http.post(`OrderRows/addToOrder`, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.hide();
if (this.card) this.card.reload();
});
return true;
}
}

View File

@ -0,0 +1,171 @@
import './index.js';
describe('Order', () => {
describe('Component vnOrderPricesPopover', () => {
let controller;
let $httpBackend;
let orderId = 16;
beforeEach(ngModule('order'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $scope = $rootScope.$new();
const $element = angular.element('<vn-order-prices-popover></vn-order-prices-popover>');
const $transclude = {
$$boundTransclude: {
$$slots: []
}
};
controller = $componentController('vnOrderPricesPopover', {$element, $scope, $transclude});
controller._prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
controller.item = {available: 1000};
controller.order = {id: orderId};
}));
describe('prices() setter', () => {
it('should call to the getTotalQuantity() method', () => {
controller.getTotalQuantity = jest.fn();
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 0},
];
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
});
});
describe('getTotalQuantity()', () => {
it('should set the totalQuantity and maxQuantity properties', () => {
controller.getTotalQuantity();
expect(controller.totalQuantity).toEqual(100);
expect(controller.maxQuantity).toEqual(900);
});
});
describe('addQuantity()', () => {
it('should call to the getTotalQuantity() method and NOT set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{grouping: 10, quantity: 0},
{grouping: 100, quantity: 0},
{grouping: 1000, quantity: 1000},
];
const oneThousandGrouping = controller.prices[2];
expect(oneThousandGrouping.quantity).toEqual(1000);
controller.addQuantity(oneThousandGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneThousandGrouping.quantity).toEqual(1000);
});
it('should call to the getTotalQuantity() method and then set the quantity property', () => {
jest.spyOn(controller, 'getTotalQuantity');
const oneHandredGrouping = controller.prices[1];
controller.addQuantity(oneHandredGrouping);
expect(controller.getTotalQuantity).toHaveBeenCalledWith();
expect(oneHandredGrouping.quantity).toEqual(200);
});
});
describe('getGroupings()', () => {
it('should return a row with the total filled quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 100},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
expect(rows.length).toEqual(1);
expect(firstRow.quantity).toEqual(1110);
});
it('should return two filled rows with a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 10},
{warehouseFk: 2, grouping: 10, quantity: 10},
{warehouseFk: 1, grouping: 100, quantity: 0},
{warehouseFk: 1, grouping: 1000, quantity: 1000},
];
const rows = controller.getGroupings();
const firstRow = rows[0];
const secondRow = rows[1];
expect(rows.length).toEqual(2);
expect(firstRow.quantity).toEqual(1010);
expect(secondRow.quantity).toEqual(10);
});
});
describe('submit()', () => {
it('should throw an error if none of the rows contains a quantity', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 0}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`First you must add some quantity`);
});
it(`should throw an error if the quantity doesn't match the grouping value`, () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showError');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 101}
];
controller.submit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The amounts doesn't match with the grouping`);
});
it('should should make an http query and then show a success message', () => {
jest.spyOn(controller, 'getTotalQuantity');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.prices = [
{warehouseFk: 1, grouping: 10, quantity: 0},
{warehouseFk: 1, grouping: 100, quantity: 100}
];
const params = {
orderFk: orderId,
items: [{warehouseFk: 1, grouping: 100, quantity: 100}]
};
$httpBackend.expectPOST('OrderRows/addToOrder', params).respond(200);
controller.submit();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith(`Data saved!`);
});
});
});
});

View File

@ -1,4 +1,3 @@
Last entries: Últimas entradas
Qty.: Cant.
Wrong quantity: Cantidad errónea
First you must add some quantity: Primero debes agregar alguna cantidad
First you must add some quantity: Primero debes agregar alguna cantidad
The amounts doesn't match with the grouping: Las cantidades no coinciden con el grouping

View File

@ -1,47 +1,18 @@
@import "variables";
.vn-order-prices-popover .content {
max-width: 350px;
.header > a:first-child {
visibility: hidden;
}
img[ng-src] {
height: 100%;
width: 100%;
}
vn-vertical.data {
padding-right: 16px;
border-right: 1px solid $color-main;
}
.prices {
vn-table {
.warehouse {
width: 48px;
max-width: 48px;
}
.price-kg {
color: $color-font-secondary;
font-size: .75rem
}
.vn-input-number {
width: 56px;
width: 80px;
}
}
.footer {
text-align: center;
.error {
display: none;
}
&.invalid {
.error {
display: block;
padding-top: 10px;
color: $color-alert;
text-align: center;
}
}
}
}
}

View File

@ -0,0 +1,64 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('loopback model Supplier-account', () => {
describe('create', () => {
const supplierId = 1;
const bankEntityId = 2100;
it('should throw an error when attempting to set an invalid iban account', async() => {
let error;
const expectedError = 'The IBAN does not have the correct format';
const iban = 'incorrect format';
try {
await app.models.SupplierAccount.create(
{
supplierFk: supplierId,
bankEntityFk: bankEntityId,
iban: iban
});
} catch (e) {
error = e;
expect(error.message).toContain(expectedError);
}
expect(error).toBeDefined();
});
it('should create a valid supplier account', async() => {
const tx = await app.models.Claim.beginTransaction({});
try {
const options = {transaction: tx};
const iban = 'ES91 2100 0418 4502 0005 1332';
const activeCtx = {
accessToken: {userId: 5},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
activeCtx.http.req.__ = value => {
return value;
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const createdSupplierAccount = await app.models.SupplierAccount.create({
supplierFk: supplierId,
bankEntityFk: bankEntityId,
iban: iban
},
options);
expect(createdSupplierAccount.iban).toBe(iban);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server');
describe('loopback model address', () => {
describe('loopback model Supplier', () => {
let supplierOne;
let supplierTwo;

View File

@ -0,0 +1,22 @@
const validateIban = require('vn-loopback/util/validateIban');
module.exports = Self => {
Self.validateAsync('iban', ibanValidation, {
message: 'The IBAN does not have the correct format'
});
async function ibanValidation(err, done) {
let filter = {
fields: ['code'],
where: {id: this.countryFk}
};
let country = await Self.app.models.Country.findOne(filter);
let code = country ? country.code.toLowerCase() : null;
if (code != 'es')
return done();
if (!validateIban(this.iban))
err();
done();
}
};

View File

@ -7,7 +7,7 @@
},
"options": {
"mysql": {
"table": "supplierAccount"
"table": "supplierAccount"
}
},
"properties": {
@ -16,39 +16,18 @@
"id": true,
"description": "Identifier"
},
"supplierFk": {
"type": "Number"
},
"iban": {
"type": "String"
},
"office": {
"beneficiary": {
"type": "String"
},
"DC": {
"type": "String"
},
"number": {
"type": "String"
},
"description": {
"type": "String"
},
"bicSufix": {
"type": "String"
},
"bankEntityFk": {
"type": "Number"
},
"bankFk": {
"type": "Number"
}
},
"relations": {
"supplier": {
"type": "belongsTo",
"model": "Supplier",
"foreignKey": "supplierFk"
"foreignKey": "supplierFk"
},
"bankEntity": {
"type": "belongsTo",

View File

@ -1,7 +1,7 @@
<vn-crud-model
vn-id="model"
url="SupplierAccounts"
fields="['id', 'supplierFk', 'iban', 'bankEntityFk']"
fields="['id', 'supplierFk', 'iban', 'bankEntityFk', 'beneficiary']"
link="{supplierFk: $ctrl.$params.id}"
include="$ctrl.include"
data="$ctrl.supplierAccounts"
@ -12,7 +12,7 @@
data="$ctrl.supplierAccounts"
form="form">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-lg">
<vn-card class="vn-pa-lg">
<vn-horizontal ng-repeat="supplierAccount in $ctrl.supplierAccounts">
<vn-textfield vn-three
@ -21,7 +21,7 @@
ng-model="supplierAccount.iban"
rule>
</vn-textfield>
<vn-autocomplete vn-two
<vn-autocomplete vn-three
label="Bank entity"
ng-model="supplierAccount.bankEntityFk"
url="BankEntities"
@ -35,6 +35,11 @@
ng-click="$ctrl.showBankEntity($event, $index)">
</vn-icon-button>
</append>
<vn-textfield vn-three
label="Beneficiary"
ng-model="supplierAccount.beneficiary"
info="Beneficiary information">
</vn-textfield>
<vn-none>
<vn-icon-button
vn-tooltip="Remove account"

View File

@ -0,0 +1 @@
Beneficiary information: Name of the bank account holder if different from the provider

View File

@ -1,3 +1,5 @@
Bank entity: Entidad bancaria
swift: Swift BIC
Add account: Añadir cuenta
Add account: Añadir cuenta
Beneficiary: Beneficiario
Beneficiary information: Nombre del titular de la cuenta bancaria en caso de ser diferente del proveedor

View File

@ -1,4 +1,5 @@
const app = require('vn-loopback/server/server');
const soap = require('soap');
describe('ticket sendSms()', () => {
let logId;
@ -10,6 +11,7 @@ describe('ticket sendSms()', () => {
});
it('should send a message and log it', async() => {
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
let ctx = {req: {accessToken: {userId: 9}}};
let id = 11;
let destination = 222222222;

View File

@ -1,16 +1,13 @@
const UserError = require('vn-loopback/util/user-error');
const LoopBackContext = require('loopback-context');
module.exports = Self => {
Self.observe('before save', async ctx => {
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const models = Self.app.models;
let changes = ctx.currentInstance || ctx.instance;
if (changes) {
let ticketId = changes.ticketFk;
let isEditable = await models.Ticket.isEditable(httpCtx, ticketId);
if (!isEditable)
let isLocked = await models.Ticket.isLocked(ticketId);
if (isLocked)
throw new UserError(`The current ticket can't be modified`);
if (changes.ticketServiceTypeFk) {
@ -21,13 +18,11 @@ module.exports = Self => {
});
Self.observe('before delete', async ctx => {
const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const models = Self.app.models;
const service = await models.TicketService.findById(ctx.where.id);
const isEditable = await models.Ticket.isEditable(httpCtx, service.ticketFk);
const isLocked = await models.Ticket.isLocked(service.ticketFk);
if (!isEditable)
if (isLocked)
throw new UserError(`The current ticket can't be modified`);
});
};

View File

@ -7,7 +7,6 @@
<vn-item
ng-click="addTurn.show()"
vn-acl="buyer"
ng-show="$ctrl.isEditable"
vn-acl-action="remove"
name="addTurn"
translate>

View File

@ -18,6 +18,7 @@
<vn-th field="name">Name</vn-th>
<vn-th field="isBox">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="worker">Worker</vn-th>
<vn-th field="created" expand>Created</vn-th>
</vn-tr>
@ -41,6 +42,7 @@
<vn-td>{{::expedition.packageItemName}}</vn-td>
<vn-td>{{::expedition.freightItemName}}</vn-td>
<vn-td number>{{::expedition.counter}}</vn-td>
<vn-td number>{{::expedition.externalId}}</vn-td>
<vn-td expand>
<span
class="link"

6
package-lock.json generated
View File

@ -17073,9 +17073,9 @@
}
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"