4077-login_recover-password & account_verifyEmail #1063

Merged
alexm merged 52 commits from 4077-login_recover-password into dev 2022-11-28 11:34:03 +00:00
132 changed files with 3260 additions and 751 deletions
Showing only changes of commit 36894c31de - Show all commits

View File

@ -1,5 +0,0 @@
ALTER TABLE `vn`.`itemType` CHANGE `transaction` transaction__ tinyint(4) DEFAULT 0 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE location location__ varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;
ALTER TABLE `vn`.`itemType` CHANGE hasComponents hasComponents__ tinyint(1) DEFAULT 1 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE warehouseFk warehouseFk__ smallint(6) unsigned DEFAULT 60 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE compression compression__ decimal(5,2) DEFAULT 1.00 NULL;

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Business', '*', '*', 'ALLOW', 'ROLE', 'hr');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Sale', 'usesMana', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative'),

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ItemShelving', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('ItemShelving', '*', 'WRITE', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ItemShelvingPlacementSupplyStock', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,54 @@
DROP PROCEDURE IF EXISTS `vn`.`zone_getPostalCode`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`zone_getPostalCode`(vSelf INT)
BEGIN
/**
* Devuelve los códigos postales incluidos en una zona
*/
DECLARE vGeoFk INT DEFAULT NULL;
DROP TEMPORARY TABLE IF EXISTS tmp.zoneNodes;
CREATE TEMPORARY TABLE tmp.zoneNodes (
geoFk INT,
name VARCHAR(100),
parentFk INT,
sons INT,
isChecked BOOL DEFAULT 0,
zoneFk INT,
PRIMARY KEY zoneNodesPk (zoneFk, geoFk),
INDEX(geoFk))
ENGINE = MEMORY;
CALL zone_getLeaves2(vSelf, NULL , NULL);
UPDATE tmp.zoneNodes zn
SET isChecked = 0
WHERE parentFk IS NULL;
myLoop: LOOP
SET vGeoFk = NULL;
SELECT geoFk INTO vGeoFk
FROM tmp.zoneNodes zn
WHERE NOT isChecked
LIMIT 1;
CALL zone_getLeaves2(vSelf, vGeoFk, NULL);
UPDATE tmp.zoneNodes
SET isChecked = TRUE
WHERE geoFk = vGeoFk;
IF vGeoFk IS NULL THEN
LEAVE myLoop;
END IF;
END LOOP;
DELETE FROM tmp.zoneNodes
WHERE sons > 0;
SELECT zn.geoFk, zn.name
FROM tmp.zoneNodes zn
JOIN zone z ON z.id = zn.zoneFk;
END$$
DELIMITER ;

View File

@ -353,48 +353,48 @@ INSERT INTO `vn`.`clientConfig`(`riskTolerance`, `maxCreditRows`)
INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
VALUES
(1, 'Bruce Wayne', '1007 Mountain Drive, Gotham', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 1),
(2, 'Petter Parker', '20 Ingram Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 1),
(3, 'Clark Kent', '344 Clinton Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 1),
(4, 'Tony Stark', '10880 Malibu Point', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 1),
(5, 'Max Eisenhardt', 'Unknown Whereabouts', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 1),
(6, 'DavidCharlesHaller', 'Evil hideout', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 1),
(7, 'Hank Pym', 'Anthill', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 1),
(8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1),
(9, 'Bruce Banner', 'Somewhere in New York', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 1),
(10, 'Jessica Jones', 'NYCC 2015 Poster', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 1),
(11, 'Missing', 'The space', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1111, 10, NULL, NULL, 0, 1),
(12, 'Trash', 'New York city', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1112, 10, NULL, NULL, 0, 1),
(101, 'Somewhere in Thailand', 'address 01', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(102, 'Somewhere in Poland', 'address 02', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(103, 'Somewhere in Japan', 'address 03', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(104, 'Somewhere in Spain', 'address 04', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(105, 'Somewhere in Potugal', 'address 05', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(106, 'Somewhere in UK', 'address 06', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(107, 'Somewhere in Valencia', 'address 07', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(108, 'Somewhere in Gotham', 'address 08', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(109, 'Somewhere in London', 'address 09', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(110, 'Somewhere in Algemesi', 'address 10', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(111, 'Somewhere in Carlet', 'address 11', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(112, 'Somewhere in Campanar', 'address 12', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(113, 'Somewhere in Malilla', 'address 13', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(114, 'Somewhere in France', 'address 14', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(115, 'Somewhere in Birmingham', 'address 15', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(116, 'Somewhere in Scotland', 'address 16', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(117, 'Somewhere in nowhere', 'address 17', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(118, 'Somewhere over the rainbow', 'address 18', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(119, 'Somewhere in Alberic', 'address 19', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(120, 'Somewhere in Montortal', 'address 20', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(121, 'the bat cave', 'address 21', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 0),
(122, 'NY roofs', 'address 22', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 0),
(123, 'The phone box', 'address 23', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 0),
(124, 'Stark tower Gotham', 'address 24', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 0),
(125, 'The plastic cell', 'address 25', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 0),
(126, 'Many places', 'address 26', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 0),
(127, 'Your pocket', 'address 27', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 0),
(128, 'Cerebro', 'address 28', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 0),
(130, 'Non valid address', 'address 30', 'Gotham', 46460, 1, 1111111111, 222222222, 0, 1101, 2, NULL, NULL, 0, 0);
(1, 'Bruce Wayne', '1007 Mountain Drive, Gotham', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 1),
(2, 'Petter Parker', '20 Ingram Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 1),
(3, 'Clark Kent', '344 Clinton Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 1),
(4, 'Tony Stark', '10880 Malibu Point', 'Gotham', 46460, 1, 1111111111, 222222222, 1 , 1104, 2, NULL, NULL, 0, 1),
(5, 'Max Eisenhardt', 'Unknown Whereabouts', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 1),
(6, 'DavidCharlesHaller', 'Evil hideout', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 1),
(7, 'Hank Pym', 'Anthill', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 1),
(8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1),
(9, 'Bruce Banner', 'Somewhere in New York', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 1),
(10, 'Jessica Jones', 'NYCC 2015 Poster', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 1),
(11, 'Missing', 'The space', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1111, 10, NULL, NULL, 0, 1),
(12, 'Trash', 'New York city', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1112, 10, NULL, NULL, 0, 1),
(101, 'Somewhere in Thailand', 'address 01', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(102, 'Somewhere in Poland', 'address 02', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(103, 'Somewhere in Japan', 'address 03', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(104, 'Somewhere in Spain', 'address 04', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(105, 'Somewhere in Potugal', 'address 05', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(106, 'Somewhere in UK', 'address 06', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(107, 'Somewhere in Valencia', 'address 07', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(108, 'Somewhere in Gotham', 'address 08', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(109, 'Somewhere in London', 'address 09', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(110, 'Somewhere in Algemesi', 'address 10', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(111, 'Somewhere in Carlet', 'address 11', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(112, 'Somewhere in Campanar', 'address 12', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(113, 'Somewhere in Malilla', 'address 13', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(114, 'Somewhere in France', 'address 14', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(115, 'Somewhere in Birmingham', 'address 15', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(116, 'Somewhere in Scotland', 'address 16', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(117, 'Somewhere in nowhere', 'address 17', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(118, 'Somewhere over the rainbow', 'address 18', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(119, 'Somewhere in Alberic', 'address 19', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(120, 'Somewhere in Montortal', 'address 20', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(121, 'the bat cave', 'address 21', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 0),
(122, 'NY roofs', 'address 22', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 0),
(123, 'The phone box', 'address 23', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 0),
(124, 'Stark tower Gotham', 'address 24', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 0),
(125, 'The plastic cell', 'address 25', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 0),
(126, 'Many places', 'address 26', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 0),
(127, 'Your pocket', 'address 27', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 0),
(128, 'Cerebro', 'address 28', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Gotham', 'EC170150', 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 0),
(130, 'Non valid address', 'address 30', 'Gotham', 46460, 1, 1111111111, 222222222, 0, 1101, 2, NULL, NULL, 0, 0);
INSERT INTO `vn`.`address`( `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `isActive`, `clientFk`, `agencyModeFk`, `isDefaultAddress`)
SELECT name, CONCAT(name, 'Street'), 'GOTHAM', 46460, 1, 1, id, 2, 1
@ -2047,6 +2047,7 @@ INSERT INTO `vn`.`zoneIncluded` (`zoneFk`, `geoFk`, `isIncluded`)
(8, 4, 0),
(8, 5, 0),
(8, 1, 1),
(9, 7, 1),
(10, 14, 1);
INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `dated`)

View File

@ -1,37 +0,0 @@
const app = require('vn-loopback/server/server');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
// #1885
xdescribe('order_confirmWithUser()', () => {
it('should confirm an order', async() => {
let stmts = [];
let stmt;
stmts.push('START TRANSACTION');
let params = {
orderFk: 10,
userId: 9
};
// problema: la funcion order_confirmWithUser tiene una transacción, por tanto esta nunca hace rollback
stmt = new ParameterizedSQL('CALL hedera.order_confirmWithUser(?, ?)', [
params.orderFk,
params.userId
]);
stmts.push(stmt);
stmt = new ParameterizedSQL('SELECT confirmed FROM hedera.order WHERE id = ?', [
params.orderFk
]);
let orderIndex = stmts.push(stmt) - 1;
stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql);
savedDescription = result[orderIndex][0].confirmed;
expect(savedDescription).toBeTruthy();
});
});

View File

@ -394,11 +394,18 @@ export default {
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
advancedSearchItemType: 'vn-item-search-panel vn-autocomplete[ng-model="filter.typeFk"]',
advancedSearchButton: 'vn-item-search-panel button[type=submit]',
advancedSmartTableButton: 'vn-item-index vn-button[icon="search"]',
advancedSmartTableGrouping: 'vn-item-index vn-textfield[name=grouping]',
weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]',
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
},
itemFixedPrice: {
add: 'vn-fixed-price vn-icon-button[icon="add_circle"]',
firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]',
fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)',
fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]',
fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]',
@ -408,7 +415,8 @@ export default {
fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]',
fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]',
fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]',
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]'
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]',
orderColumnId: 'vn-fixed-price th[field="itemFk"]'
},
itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
@ -596,7 +604,14 @@ export default {
submitNotesButton: 'button[type=submit]'
},
ticketExpedition: {
thirdExpeditionRemoveButton: 'vn-ticket-expedition vn-table div > vn-tbody > vn-tr:nth-child(3) > vn-td:nth-child(1) > vn-icon-button[icon="delete"]',
firstSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(1) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(3) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="keyboard_arrow_down"]',
moreMenuWithoutRoute: 'vn-item[name="withoutRoute"]',
moreMenuWithRoute: 'vn-item[name="withRoute"]',
newRouteId: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newRoute"]',
saveButton: '.vn-dialog.shown [response="accept"]',
expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr'
},
ticketPackages: {
@ -1100,7 +1115,8 @@ export default {
anyBuyLine: 'vn-entry-summary tr.dark-row'
},
entryBasicData: {
reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.ref"]',
reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.reference"]',
invoiceNumber: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.invoiceNumber"]',
notes: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.notes"]',
observations: 'vn-entry-basic-data vn-textarea[ng-model="$ctrl.entry.observation"]',
supplier: 'vn-entry-basic-data vn-autocomplete[ng-model="$ctrl.entry.supplierFk"]',

View File

@ -0,0 +1,77 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('SmartTable SearchBar integration', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('salesPerson', 'item');
await page.waitToClick(selectors.globalItems.searchButton);
});
afterAll(async() => {
await browser.close();
});
describe('as filters', () => {
it('should search by type in searchBar', async() => {
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should search by grouping in smartTable', async() => {
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
it('should now reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
});
describe('as orders', () => {
it('should order by first id', async() => {
await page.loginAndModule('developer', 'item');
await page.accessToSection('item.fixedPrice');
await page.doSearch();
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('1');
});
it('should order by last id', async() => {
await page.waitToClick(selectors.itemFixedPrice.orderColumnId);
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
it('should reload page and have same order', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
});
});

View File

@ -18,7 +18,8 @@ describe('Ticket expeditions and log path', () => {
});
it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdExpeditionRemoveButton);
await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.deleteExpeditionButton);
await page.waitToClick(selectors.globalItems.acceptButton);
await page.reloadSection('ticket.card.expedition');

View File

@ -0,0 +1,50 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Ticket expeditions', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('production', 'ticket');
await page.accessToSearchResult('1');
await page.accessToSection('ticket.card.expedition');
});
afterAll(async() => {
await browser.close();
});
it(`should move one expedition to new ticket withoute route`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithoutRoute);
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
it(`should move one expedition to new ticket with route`, async() => {
await page.waitToClick(selectors.ticketExpedition.firstSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithRoute);
await page.write(selectors.ticketExpedition.newRouteId, '1');
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
});

View File

@ -19,6 +19,7 @@ describe('Entry basic data path', () => {
it('should edit the basic data', async() => {
await page.write(selectors.entryBasicData.reference, 'new movement 8');
await page.write(selectors.entryBasicData.invoiceNumber, 'new movement 8');
await page.write(selectors.entryBasicData.notes, 'new notes');
await page.write(selectors.entryBasicData.observations, ' edited');
await page.autocompleteSearch(selectors.entryBasicData.supplier, 'Plants nick');
@ -45,6 +46,13 @@ describe('Entry basic data path', () => {
expect(result).toEqual('new movement 8');
});
it('should confirm the invoiceNumber was edited', async() => {
await page.reloadSection('entry.card.basicData');
const result = await page.waitToGetProperty(selectors.entryBasicData.invoiceNumber, 'value');
expect(result).toEqual('new movement 8');
});
it('should confirm the note was edited', async() => {
const result = await page.waitToGetProperty(selectors.entryBasicData.notes, 'value');

View File

@ -99,6 +99,18 @@ export default class CrudModel extends ModelProxy {
return this.refresh();
}
/**
* Applies a new filter to the model.
*
* @param {Object} params Custom parameters
* @return {Promise} The request promise
*/
applyParams(params) {
this.userParams = params;
return this.refresh();
}
removeFilter() {
return this.applyFilter(null, null);
}

View File

@ -139,8 +139,12 @@ export default class Searchbar extends Component {
}
removeParam(index) {
const field = this.params[index].key;
this.filterSanitizer(field);
this.params.splice(index, 1);
this.doSearch(this.fromBar(), 'bar');
this.toRemove = field;
this.doSearch(this.fromBar(), 'removeBar');
}
fromBar() {
@ -163,7 +167,7 @@ export default class Searchbar extends Component {
let keys = Object.keys(filter);
keys.forEach(key => {
if (key == 'search') return;
if (key == 'search' || key == 'tableQ' || key == 'tableOrder') return;
let value = filter[key];
let chip;
@ -198,6 +202,7 @@ export default class Searchbar extends Component {
let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data));
this.toBar(filter);
}
onFilter(filter, source, data) {
@ -238,8 +243,11 @@ export default class Searchbar extends Component {
} else {
state = this.searchState;
if (filter)
if (filter) {
if (this.tableQ)
filter.tableQ = this.tableQ;
params = {q: JSON.stringify(filter)};
}
if (this.$state.is(state))
opts = {location: 'replace'};
}
@ -247,6 +255,12 @@ export default class Searchbar extends Component {
this.filter = filter;
if (source == 'removeBar') {
delete params[this.toRemove];
delete this.model.userParams[this.toRemove];
this.model.refresh();
}
if (!filter && this.model)
this.model.clear();
if (source != 'state')
@ -269,9 +283,14 @@ export default class Searchbar extends Component {
this.model.clear();
return;
}
if (Object.keys(filter).length === 0) {
this.filterSanitizer('search');
if (this.model.userParams)
delete this.model.userParams['search'];
}
let where = null;
let params = null;
let params = {};
if (this.exprBuilder) {
where = buildFilter(filter,
@ -283,9 +302,89 @@ export default class Searchbar extends Component {
params = this.fetchParams({$params: params});
}
this.tableQ = null;
const hasParams = this.$params.q && Object.keys(JSON.parse(this.$params.q)).length;
if (hasParams) {
const stateFilter = JSON.parse(this.$params.q);
for (let param in stateFilter) {
if (param != 'tableQ' && param != 'orderQ')
this.filterSanitizer(param);
}
for (let param in this.suggestedFilter) {
this.filterSanitizer(param);
delete stateFilter[param];
}
this.tableQ = stateFilter.tableQ;
for (let param in stateFilter.tableQ)
params[param] = stateFilter.tableQ[param];
Object.assign(stateFilter, params);
return this.model.applyParams(params)
.then(() => this.model.data);
}
return this.model.applyFilter(where ? {where} : null, params)
.then(() => this.model.data);
}
filterSanitizer(field) {
if (!field) return;
const userFilter = this.model.userFilter;
const userParams = this.model.userParams;
const where = userFilter && userFilter.where;
if (this.model.userParams)
delete this.model.userParams[field];
if (this.exprBuilder) {
const param = this.exprBuilder({
param: field,
value: null
});
if (param) [field] = Object.keys(param);
}
if (!where) return;
const whereKeys = Object.keys(where);
for (let key of whereKeys) {
removeProp(where, field, key);
if (Object.keys(where).length == 0)
delete userFilter.where;
}
function removeProp(obj, targetProp, prop) {
if (prop == targetProp)
delete obj[prop];
if (prop === 'and' || prop === 'or' && obj[prop]) {
const arrayCopy = obj[prop].slice();
for (let param of arrayCopy) {
const [key] = Object.keys(param);
const index = obj[prop].findIndex(param => {
return Object.keys(param)[0] == key;
});
if (key == targetProp)
obj[prop].splice(index, 1);
if (param[key] instanceof Array)
removeProp(param, field, key);
if (Object.keys(param).length == 0)
obj[prop].splice(index, 1);
}
if (obj[prop].length == 0)
delete obj[prop];
}
}
return {userFilter, userParams};
}
}
ngModule.vnComponent('vnSearchbar', {

View File

@ -6,7 +6,7 @@ describe('Component vnSearchbar', () => {
let $state;
let $params;
let $scope;
let filter = {id: 1, search: 'needle'};
const filter = {id: 1, search: 'needle'};
beforeEach(ngModule('vnCore', $stateProvider => {
$stateProvider
@ -70,8 +70,8 @@ describe('Component vnSearchbar', () => {
describe('filter() setter', () => {
it(`should update the bar params and search`, () => {
let withoutHours = new Date(2000, 1, 1);
let withHours = new Date(withoutHours.getTime());
const withoutHours = new Date(2000, 1, 1);
const withHours = new Date(withoutHours.getTime());
withHours.setHours(12, 30, 15, 10);
controller.filter = {
@ -83,8 +83,8 @@ describe('Component vnSearchbar', () => {
myObjectProp: {myProp: 1}
};
let chips = {};
for (let param of controller.params || [])
const chips = {};
for (const param of controller.params || [])
chips[param.key] = param.chip;
expect(controller.searchString).toBe('needle');
@ -172,13 +172,22 @@ describe('Component vnSearchbar', () => {
describe('removeParam()', () => {
it(`should remove the parameter from the filter`, () => {
jest.spyOn(controller, 'doSearch');
controller.model = {
refresh: jest.fn(),
userParams: {
id: 1
}
};
controller.model.applyParams = jest.fn().mockReturnValue(Promise.resolve());
jest.spyOn(controller.model, 'applyParams');
controller.filter = filter;
controller.removeParam(0);
expect(controller.doSearch).toHaveBeenCalledWith({
search: 'needle'
}, 'bar');
}, 'removeBar');
});
});
@ -199,7 +208,7 @@ describe('Component vnSearchbar', () => {
it(`should go to the summary state when one result`, () => {
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -214,7 +223,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply();
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -229,7 +238,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply();
jest.spyOn($state, 'go');
let data = [{id: 1}];
const data = [{id: 1}];
controller.baseState = 'foo';
controller.onFilter(filter, 'any', data);
@ -247,7 +256,7 @@ describe('Component vnSearchbar', () => {
controller.onFilter(filter, 'any');
$scope.$apply();
let queryParams = {q: JSON.stringify(filter)};
const queryParams = {q: JSON.stringify(filter)};
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined);
expect(controller.filter).toEqual(filter);

View File

@ -103,3 +103,4 @@
</div>
</tpl-body>
</vn-popover>

View File

@ -15,9 +15,17 @@ export default class SmartTable extends Component {
this.$inputsScope;
this.columns = [];
this.autoSave = false;
this.autoState = true;
this.transclude();
}
$onChanges() {
if (this.model) {
this.defaultFilter();
this.defaultOrder();
}
}
$onDestroy() {
const styleElement = document.querySelector('style[id="smart-table"]');
if (this.$.css && styleElement)
@ -47,10 +55,8 @@ export default class SmartTable extends Component {
set model(value) {
this._model = value;
if (value) {
if (value)
this.$.model = value;
this.defaultOrder();
}
}
getDefaultViewConfig() {
@ -160,8 +166,36 @@ export default class SmartTable extends Component {
}
}
defaultFilter() {
if (this.disabledTableFilter || !this.$params.q) return;
const stateFilter = JSON.parse(this.$params.q).tableQ;
if (!stateFilter || !this.exprBuilder) return;
const columns = this.columns.map(column => column.field);
this.displaySearch();
if (!this.$inputsScope.searchProps)
this.$inputsScope.searchProps = {};
for (let param in stateFilter) {
if (columns.includes(param)) {
const whereParams = {[param]: stateFilter[param]};
Object.assign(this.$inputsScope.searchProps, whereParams);
this.addFilter(param, stateFilter[param]);
}
}
}
defaultOrder() {
const order = this.model.order;
if (this.disabledTableOrder) return;
let stateOrder;
if (this.$params.q)
stateOrder = JSON.parse(this.$params.q).tableOrder;
const order = stateOrder ? stateOrder : this.model.order;
if (!order) return;
const orderFields = order.split(', ');
@ -195,6 +229,9 @@ export default class SmartTable extends Component {
this.setPriority(column.element, priority);
}
}
this.model.order = order;
this.refresh();
}
registerColumns() {
@ -395,28 +432,54 @@ export default class SmartTable extends Component {
}
searchByColumn(field) {
const searchCriteria = this.$inputsScope.searchProps[field];
const emptySearch = searchCriteria === '' || searchCriteria == null;
const filters = this.filterSanitizer(field);
if (filters && filters.userFilter)
this.model.userFilter = filters.userFilter;
if (!emptySearch)
this.addFilter(field, this.$inputsScope.searchProps[field]);
else this.model.refresh();
this.addFilter(field, this.$inputsScope.searchProps[field]);
}
searchPropsSanitizer() {
if (!this.$inputsScope || !this.$inputsScope.searchProps) return null;
let searchProps = this.$inputsScope.searchProps;
const searchPropsArray = Object.entries(searchProps);
searchProps = searchPropsArray.filter(
([key, value]) => value && value != ''
);
return Object.fromEntries(searchProps);
}
addFilter(field, value) {
let where = {[field]: value};
if (value == '') value = null;
if (this.exprBuilder) {
where = buildFilter(where, (param, value) =>
this.exprBuilder({param, value})
);
let stateFilter = {tableQ: {}};
if (this.$params.q) {
stateFilter = JSON.parse(this.$params.q);
if (!stateFilter.tableQ)
stateFilter.tableQ = {};
delete stateFilter.tableQ[field];
}
this.model.addFilter({where});
const whereParams = {[field]: value};
if (value) {
let where = {[field]: value};
if (this.exprBuilder) {
where = buildFilter(whereParams, (param, value) =>
this.exprBuilder({param, value})
);
}
this.model.addFilter({where});
}
const searchProps = this.searchPropsSanitizer();
Object.assign(stateFilter.tableQ, searchProps);
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
}
applySort() {
@ -426,7 +489,18 @@ export default class SmartTable extends Component {
if (order)
this.model.order = order;
this.model.refresh();
let stateFilter = {tableOrder: {}};
if (this.$params.q) {
stateFilter = JSON.parse(this.$params.q);
if (!stateFilter.tableOrder)
stateFilter.tableOrder = {};
}
stateFilter.tableOrder = order;
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
}
filterSanitizer(field) {
@ -535,6 +609,8 @@ ngModule.vnComponent('smartTable', {
autoSave: '<?',
exprBuilder: '&?',
defaultNewData: '&?',
options: '<?'
options: '<?',
disabledTableFilter: '<?',
disabledTableOrder: '<?',
}
});

View File

@ -9,6 +9,11 @@ describe('Component smartTable', () => {
$httpBackend = _$httpBackend_;
$element = $compile(`<smart-table></smart-table>`)($rootScope);
controller = $element.controller('smartTable');
controller.model = {
refresh: jest.fn().mockReturnValue(new Promise(resolve => resolve())),
addFilter: jest.fn(),
userParams: {}
};
}));
afterEach(() => {
@ -83,7 +88,7 @@ describe('Component smartTable', () => {
describe('defaultOrder', () => {
it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => {
const element = document.createElement('div');
controller.model = {order: 'id'};
controller.model.order = 'id';
controller.columns = [
{field: 'id', element: element},
{field: 'test1', element: element},
@ -101,7 +106,8 @@ describe('Component smartTable', () => {
it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
const element = document.createElement('div');
controller.model = {order: 'test1, id DESC'};
controller.model.order = 'test1, id DESC';
controller.columns = [
{field: 'id', element: element},
{field: 'test1', element: element},
@ -125,8 +131,6 @@ describe('Component smartTable', () => {
describe('addFilter()', () => {
it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => {
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue');
const expectedFilter = {
@ -140,7 +144,6 @@ describe('Component smartTable', () => {
it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue');
@ -155,35 +158,48 @@ describe('Component smartTable', () => {
});
describe('applySort()', () => {
it('should call the model refresh() without making changes on the model order', () => {
controller.model = {refresh: jest.fn()};
it('should call the $state go and model refresh without making changes on the model order', () => {
controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.applySort();
expect(controller.model.order).toBeUndefined();
expect(controller.model.refresh).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
it('should call the model.refresh() after setting model order according to the controller sortCriteria', () => {
controller.model = {refresh: jest.fn()};
it('should call the $state go and model refresh after setting model order according to the controller sortCriteria', () => {
const orderBy = {field: 'myField', sortType: 'ASC'};
controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.sortCriteria = [orderBy];
controller.applySort();
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
expect(controller.model.refresh).toHaveBeenCalled();
expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
});
describe('filterSanitizer()', () => {
it('should remove the where filter after leaving no fields in it', () => {
controller.model = {
userFilter: {
where: {fieldToRemove: 'valueToRemove'}
},
userParams: {}
controller.model.userFilter = {
where: {fieldToRemove: 'valueToRemove'}
};
controller.model.userParams = {};
const result = controller.filterSanitizer('fieldToRemove');
@ -193,23 +209,21 @@ describe('Component smartTable', () => {
});
it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => {
controller.model = {
userFilter: {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
{
or: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
]
}
]
}
},
userParams: {}
};
controller.model.userFilter = {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
{
or: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
]
}
]
}
},
controller.model.userParams = {};
const result = controller.filterSanitizer('aFieldToRemove');
@ -219,24 +233,22 @@ describe('Component smartTable', () => {
});
it('should not remove the where filter after leaving no empty "ands/ors" in it', () => {
controller.model = {
userFilter: {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
{
or: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
]
}
],
or: [{dontKillMe: 'thanks'}]
}
},
userParams: {}
controller.model.userFilter = {
where: {
and: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
{
or: [
{aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'},
]
}
],
or: [{dontKillMe: 'thanks'}]
}
};
controller.model.userParams = {};
const result = controller.filterSanitizer('aFieldToRemove');
@ -249,7 +261,7 @@ describe('Component smartTable', () => {
describe('saveAll()', () => {
it('should throw an error if there are no changes to save in the model', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.model = {isChanged: false};
controller.model.isChanged = false;
controller.saveAll();
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
@ -258,10 +270,8 @@ describe('Component smartTable', () => {
it('should call the showSuccess() if there are changes to save in the model', done => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.model = {
save: jest.fn().mockReturnValue(Promise.resolve()),
isChanged: true
};
controller.model.save = jest.fn().mockReturnValue(Promise.resolve());
controller.model.isChanged = true;
controller.saveAll().then(() => {
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
@ -269,4 +279,43 @@ describe('Component smartTable', () => {
}).catch(done.fail);
});
});
describe('defaultFilter()', () => {
it('should call model refresh and model addFilter with filter', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.$params = {
q: '{"tableQ": {"fieldName":"value"}}'
};
controller.columns = [
{field: 'fieldName'}
];
controller.$inputsScope = {
searchProps: {}
};
jest.spyOn(controller, 'refresh');
controller.defaultFilter();
expect(controller.model.addFilter).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
});
describe('searchPropsSanitizer()', () => {
it('should searchProps sanitize', () => {
controller.$inputsScope = {
searchProps: {
filterOne: '1',
filterTwo: ''
}
};
const searchPropsExpected = {
filterOne: '1'
};
const newSearchProps = controller.searchPropsSanitizer();
expect(newSearchProps).toEqual(searchPropsExpected);
});
});
});

View File

@ -51,6 +51,7 @@ Entries: Entradas
Users: Usuarios
Suppliers: Proveedores
Monitors: Monitores
Shelvings: Carros
# Common

View File

@ -240,6 +240,7 @@
"Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador",
"Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador",
"Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente",
"This route does not exists": "Esta ruta no existe",
"Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*",
"You don't have grant privilege": "No tienes privilegios para dar privilegios",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario",

View File

@ -5,6 +5,8 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash;
module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() {
return await Self.findOne({
fields: [
@ -30,6 +32,7 @@ module.exports = Self => {
},
async syncUser(userName, info, password) {
let {
client,
accountConfig
@ -130,13 +133,14 @@ module.exports = Self => {
}));
}
if (changes.length)
if (shouldSync && changes.length)
await client.modify(dn, changes);
} else
} else if (shouldSync)
await client.add(dn, newEntry);
} else {
try {
await client.del(dn);
if (shouldSync)
await client.del(dn);
console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
@ -196,17 +200,19 @@ module.exports = Self => {
for (let group of groups) {
try {
let dn = `cn=${group},${groupDn}`;
await client.modify(dn, new ldap.Change({
operation,
modification: {memberUid: userName}
}));
if (shouldSync) {
await client.modify(dn, new ldap.Change({
operation,
modification: {memberUid: userName}
}));
}
} catch (err) {
if (err.name !== 'NoSuchObjectError')
throw err;
}
}
}
await applyOperations(deleteGroups, 'delete');
await applyOperations(addGroups, 'add');
},
@ -266,8 +272,10 @@ module.exports = Self => {
filter: 'objectClass=posixGroup'
};
let reqs = [];
await client.searchForeach(this.groupDn, opts,
o => reqs.push(client.del(o.dn)));
await client.searchForeach(this.groupDn, opts, object => {
if (shouldSync)
reqs.push(client.del(object.dn));
});
await Promise.all(reqs);
// Recreate roles
@ -291,7 +299,8 @@ module.exports = Self => {
}
let dn = `cn=${role.name},${this.groupDn}`;
reqs.push(client.add(dn, newEntry));
if (shouldSync)
reqs.push(client.add(dn, newEntry));
}
await Promise.all(reqs);
}

View File

@ -60,16 +60,19 @@ module.exports = Self => {
return `cn=Users,${dnBase}`;
},
async syncUser(userName, info, password) {
async syncUser(userName, info, password) {
let {sshClient} = this;
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub',
attributes: ['userAccountControl'],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
if (info.hasAccount) {
if (!sambaUser) {

View File

@ -73,7 +73,7 @@
{
"url": "/note",
"state": "claim.card.note",
"component": "ui-view",
"component": "ui-view",
"abstract": true,
"acl": ["salesPerson"]
},
@ -105,7 +105,7 @@
"acl": ["claimManager"]
},
{
"url": "/action",
"url": "/action?q",
"state": "claim.card.action",
"component": "vn-claim-action",
"description": "Action",
@ -131,4 +131,4 @@
"acl": ["claimManager"]
}
]
}
}

View File

@ -0,0 +1,147 @@
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 clients matched by the filter',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'object',
},
{
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by name`,
},
{
arg: 'name',
type: 'string',
description: 'The client name',
},
{
arg: 'salesPersonFk',
type: 'number',
},
{
arg: 'fi',
type: 'string',
description: 'The client fiscal id',
},
{
arg: 'socialName',
type: 'string',
},
{
arg: 'city',
type: 'string',
},
{
arg: 'postcode',
type: 'string',
},
{
arg: 'provinceFk',
type: 'number',
},
{
arg: 'email',
type: 'string',
},
{
arg: 'phone',
type: 'string',
},
{
arg: 'zoneFk',
type: 'number',
},
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
const postalCode = [];
const args = ctx.args;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (args.zoneFk) {
query = `CALL vn.zone_getPostalCode(?)`;
const [geos] = await Self.rawSql(query, [args.zoneFk]);
for (let geo of geos)
postalCode.push(geo.name);
}
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'c.id': {inq: value}}
: {'c.name': {like: `%${value}%`}};
case 'name':
case 'salesPersonFk':
case 'fi':
case 'socialName':
case 'city':
case 'postcode':
case 'provinceFk':
case 'email':
case 'phone':
param = `c.${param}`;
return {[param]: value};
case 'zoneFk':
param = 'a.postalCode';
return {[param]: {inq: postalCode}};
}
});
filter = mergeFilters(filter, {where});
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT
DISTINCT c.id,
c.name,
c.fi,
c.socialName,
c.phone,
c.city,
c.postcode,
c.email,
c.isActive,
c.isFreezed,
p.id AS provinceFk,
p.name AS province,
u.id AS salesPersonFk,
u.name AS salesPerson
FROM client c
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN province p ON p.id = c.provinceFk
JOIN vn.address a ON a.clientFk = c.id
`
);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makePagination(filter));
const clientsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return clientsIndex === 0 ? result : result[clientsIndex];
};
};

View File

@ -0,0 +1,199 @@
const {models} = require('vn-loopback/server/server');
describe('client filter()', () => {
it('should return the clients matching the filter with a limit of 20 rows', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {}};
const filter = {limit: '8'};
const result = await models.Client.filter(ctx, filter, options);
expect(result.length).toEqual(8);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the search argument with his name', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {search: 'Bruce Wayne'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the search argument with his id', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {search: '1101'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the name argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {name: 'Bruce Wayne'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "salesPersonFk" argument', async() => {
const tx = await models.Client.beginTransaction({});
const salesPersonId = 18;
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {salesPersonFk: salesPersonId}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(5);
expect(randomResultClient.salesPersonFk).toEqual(salesPersonId);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "fi" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {fi: '251628698'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstClient = result[0];
expect(result.length).toEqual(1);
expect(firstClient.name).toEqual('Max Eisenhardt');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "city" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {city: 'Gotham'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(20);
expect(randomResultClient.city.toLowerCase()).toEqual('gotham');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "postcode" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {postcode: '46460'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(20);
expect(randomResultClient.postcode).toEqual('46460');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "zoneFk" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {zoneFk: 9}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
expect(result.length).toBe(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -8,6 +8,9 @@
"BankEntity": {
"dataSource": "vn"
},
"Business": {
"dataSource": "vn"
},
"BusinessType": {
"dataSource": "vn"
},

View File

@ -0,0 +1,27 @@
{
"name": "Business",
"base": "VnModel",
"options": {
"mysql": {
"table": "business"
}
},
"properties": {
"id": {
"type": "number",
"id": true
}
},
"relations": {
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"department": {
"type": "belongsTo",
"model": "Department",
"foreignKey": "departmentFk"
}
}
}

View File

@ -47,4 +47,5 @@ module.exports = Self => {
require('../methods/client/incotermsAuthorizationHtml')(Self);
require('../methods/client/incotermsAuthorizationEmail')(Self);
require('../methods/client/consumptionSendQueued')(Self);
require('../methods/client/filter')(Self);
};

View File

@ -1,6 +1,6 @@
<vn-crud-model
vn-id="model"
url="Clients"
url="Clients/filter"
order="id DESC"
limit="8"
data="clients">
@ -10,8 +10,7 @@
vn-focus
panel="vn-client-search-panel"
info="Search client by id or name"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
model="model">
</vn-searchbar>
</vn-portal>
<vn-portal slot="menu">

View File

@ -2,32 +2,6 @@ import ngModule from '../module';
import ModuleMain from 'salix/components/module-main';
export default class Client extends ModuleMain {
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {id: value}
: {or: [{name: {like: `%${value}%`}}, {socialName: {like: `%${value}%`}}]};
case 'phone':
return {
or: [
{phone: value},
{mobile: value}
]
};
case 'name':
case 'socialName':
case 'city':
case 'email':
return {[param]: {like: `%${value}%`}};
case 'id':
case 'fi':
case 'postcode':
case 'provinceFk':
case 'salesPersonFk':
return {[param]: value};
}
}
}
ngModule.vnComponent('vnClient', {

View File

@ -20,7 +20,7 @@
{"state": "client.card.credit.index", "icon": "credit_card"},
{"state": "client.card.greuge.index", "icon": "work"},
{"state": "client.card.balance.index", "icon": "icon-invoice"},
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"},
{
@ -37,7 +37,7 @@
{"state": "client.card.unpaid", "icon": "icon-defaulter"}
]
}
]
]
},
"keybindings": [
{"key": "c", "state": "client.index"}
@ -147,7 +147,7 @@
{
"url": "/note",
"state": "client.card.note",
"component": "ui-view",
"component": "ui-view",
"abstract": true
},
{
@ -236,7 +236,7 @@
"client": "$ctrl.client"
}
},
{
{
"url": "/create?payed&companyFk&bankFk&payedAmount",
"state": "client.card.balance.create",
"component": "vn-client-balance-create",
@ -406,13 +406,13 @@
}
},
{
"url": "/defaulter",
"url": "/defaulter?q",
"state": "client.defaulter",
"component": "vn-client-defaulter",
"description": "Defaulter"
},
{
"url" : "/notification",
"url" : "/notification?q",
"state": "client.notification",
"component": "vn-client-notification",
"description": "Notifications"
@ -424,7 +424,7 @@
"description": "Unpaid"
},
{
"url": "/extended-list",
"url": "/extended-list?q",
"state": "client.extendedList",
"component": "vn-client-extended-list",
"description": "Extended list"

View File

@ -58,6 +58,13 @@
value-field="id"
label="Province">
</vn-autocomplete>
<vn-autocomplete
ng-model="filter.zoneFk"
url="Zones"
show-field="name"
value-field="id"
label="Zone">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield

View File

@ -154,7 +154,8 @@ module.exports = Self => {
e.id,
e.supplierFk,
e.dated,
e.ref,
e.ref reference,
e.ref invoiceNumber,
e.isBooked,
e.isExcludedFromAvailable,
e.notes,

View File

@ -12,10 +12,15 @@ module.exports = Self => {
http: {source: 'path'}
},
{
arg: 'ref',
arg: 'reference',
type: 'string',
description: 'The buyed boxes ids',
},
{
arg: 'invoiceNumber',
type: 'string',
description: 'The registered invoice number',
},
{
arg: 'observation',
type: 'string',
@ -63,7 +68,8 @@ module.exports = Self => {
await entry.updateAttributes({
observation: args.observation,
ref: args.ref
reference: args.reference,
invoiceNumber: args.invoiceNumber
}, myOptions);
const travel = entry.travel();

View File

@ -15,13 +15,15 @@ describe('entry import()', () => {
});
it('should import the buy rows', async() => {
const expectedRef = '1, 2';
const expectedReference = '1, 2';
const expectedInvoiceNumber = '1, 2';
const expectedObservation = '123456';
const ctx = {
req: activeCtx,
args: {
observation: expectedObservation,
ref: expectedRef,
reference: expectedReference,
invoiceNumber: expectedInvoiceNumber,
buys: [
{
itemFk: 1,
@ -58,7 +60,8 @@ describe('entry import()', () => {
}, options);
expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef);
expect(updatedEntry.reference).toEqual(expectedReference);
expect(updatedEntry.invoiceNumber).toEqual(expectedInvoiceNumber);
expect(entryBuys.length).toEqual(4);
await tx.rollback();

View File

@ -18,8 +18,17 @@
"dated": {
"type": "date"
},
"ref": {
"type": "string"
"reference": {
"type": "string",
"mysql": {
"columnName": "ref"
}
},
"invoiceNumber": {
"type": "string",
"mysql": {
"columnName": "ref"
}
},
"isBooked": {
"type": "boolean"

View File

@ -48,7 +48,7 @@
<vn-textfield
vn-one
label="Reference"
ng-model="$ctrl.entry.ref"
ng-model="$ctrl.entry.reference"
rule
vn-focus>
</vn-textfield>
@ -61,17 +61,25 @@
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
<vn-textfield
vn-one
label="Observation"
ng-model="$ctrl.entry.observation"
rule>
</vn-textarea>
label="Invoice number"
ng-model="$ctrl.entry.invoiceNumber"
rule
vn-focus>
</vn-textfield>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.entry.companyFk">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Currency"
vn-one
label="Currency"
ng-model="$ctrl.entry.currencyFk"
url="Currencies"
show-field="code"
@ -84,13 +92,14 @@
ng-model="$ctrl.entry.commission"
rule>
</vn-input-number>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.entry.companyFk">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Observation"
ng-model="$ctrl.entry.observation"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-check

View File

@ -12,6 +12,7 @@
<vn-th field="id" number>Id</vn-th>
<vn-th field="landed" center expand>Landed</vn-th>
<vn-th>Reference</vn-th>
<vn-th>Invoice number</vn-th>
<vn-th field="supplierFk">Supplier</vn-th>
<vn-th field="isBooked" center>Booked</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th>
@ -45,7 +46,8 @@
{{::entry.landed | date:'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td expand>{{::entry.ref}}</vn-td>
<vn-td expand>{{::entry.reference}}</vn-td>
<vn-td expand>{{::entry.invoiceNumber}}</vn-td>
<vn-td expand>{{::entry.supplierName}}</vn-td>
<vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td>
<vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td>

View File

@ -14,4 +14,5 @@ Booked: Contabilizada
Is inventory: Inventario
Notes: Notas
Status: Estado
Selection: Selección
Selection: Selección
Invoice number: Núm. factura

View File

@ -13,9 +13,16 @@
<vn-textfield
vn-one
label="Reference"
ng-model="filter.ref">
ng-model="filter.reference">
</vn-textfield>
<vn-textfield
vn-one
label="Invoice number"
ng-model="filter.invoiceNumber">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Travel"
ng-model="filter.travelFk">

View File

@ -5,4 +5,5 @@ From: Desde
To: Hasta
Agency: Agencia
Warehouse: Almacén
Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias
Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias
Invoice number: Núm. factura

View File

@ -27,7 +27,10 @@
value="{{$ctrl.entryData.company.code}}">
</vn-label-value>
<vn-label-value label="Reference"
value="{{$ctrl.entryData.ref}}">
value="{{$ctrl.entryData.reference}}">
</vn-label-value>
<vn-label-value label="Invoice number"
value="{{$ctrl.entryData.invoiceNumber}}">
</vn-label-value>
<vn-label-value label="Notes"
value="{{$ctrl.entryData.notes}}">

View File

@ -8,4 +8,4 @@ Minimum price: Precio mínimo
Buys: Compras
Travel: Envio
Go to the entry: Ir a la entrada
Invoice number: Núm. factura

View File

@ -0,0 +1,53 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInEmail', {
description: 'Sends the invoice in email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoice id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
},
{
arg: 'recipientId',
type: 'number',
description: 'The recipient id to send to the recipient preferred language',
required: false
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/:id/invoice-in-email',
verb: 'POST'
}
});
Self.invoiceInEmail = async ctx => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoiceIn', params);
return email.send();
};
};

View File

@ -0,0 +1,50 @@
const {Report} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInPdf', {
description: 'Returns the invoiceIn pdf',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/invoice-in-pdf',
verb: 'GET'
}
});
Self.invoiceInPdf = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {lang: ctx.req.getLocale()};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const report = new Report('invoiceIn', params);
const stream = await report.toPdfStream();
return [stream, 'application/pdf', `filename="doc-${id}.pdf"`];
};
};

View File

@ -4,4 +4,6 @@ module.exports = Self => {
require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
};

View File

@ -94,6 +94,11 @@
"model": "Supplier",
"foreignKey": "supplierFk"
},
"supplierContact": {
"type": "hasMany",
"model": "SupplierContact",
"foreignKey": "supplierFk"
},
"currency": {
"type": "belongsTo",
"model": "Currency",

View File

@ -8,6 +8,14 @@ class Controller extends ModuleCard {
{
relation: 'supplier'
},
{
relation: 'supplierContact',
scope: {
where: {
email: {neq: null}
}
}
},
{
relation: 'invoiceInDueDay'
},

View File

@ -1,5 +1,5 @@
<vn-descriptor-content
module="invoiceIn"
<vn-descriptor-content
module="invoiceIn"
description="$ctrl.invoiceIn.supplierRef"
summary="$ctrl.$.summary">
<slot-menu>
@ -10,7 +10,6 @@
translate>
To book
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="administrative"
@ -26,6 +25,16 @@
translate>
Clone Invoice
</vn-item>
<vn-item
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
</vn-item>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -37,7 +46,7 @@
</vn-label-value>
<vn-label-value label="Supplier">
<span ng-click="supplierDescriptor.show($event, $ctrl.invoiceIn.supplier.id)" class="link">
{{$ctrl.invoiceIn.supplier.nickname}}
{{$ctrl.invoiceIn.supplier.nickname}}
</span>
</vn-label-value>
</div>
@ -57,9 +66,9 @@
icon="icon-invoice-in">
</vn-quick-link>
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm
@ -75,11 +84,29 @@
<vn-supplier-descriptor-popover
vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover>
<vn-confirm
<vn-confirm
vn-id="confirm-toBookAnyway"
message="Are you sure you want to book this invoice?"
on-accept="$ctrl.onAcceptToBook()">
</vn-confirm>
<vn-popup vn-id="summary">
<vn-invoice-in-summary invoice-in="$ctrl.invoiceIn"></vn-invoice-in-summary>
</vn-popup>
</vn-popup>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -96,6 +96,20 @@ class Controller extends Descriptor {
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
}
showPdfInvoice() {
this.vnReport.show(`InvoiceIns/${this.id}/invoice-in-pdf`);
}
sendPdfInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.send(`InvoiceIns/${this.entity.id}/invoice-in-email`, {
recipient: $data.email,
recipientId: this.entity.supplier.id
});
}
}
ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -19,3 +19,5 @@ To book: Contabilizar
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF

View File

@ -0,0 +1,51 @@
module.exports = Self => {
Self.remoteMethod('deleteItemShelvings', {
description: 'Deletes the selected item shelvings',
accessType: 'WRITE',
accepts: [{
arg: 'itemShelvingIds',
type: ['number'],
required: true,
description: 'The itemShelving ids to delete'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteItemShelvings`,
verb: 'POST'
}
});
Self.deleteItemShelvings = async(itemShelvingIds, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const promises = [];
for (let itemShelvingId of itemShelvingIds) {
const itemShelvingToDelete = models.ItemShelving.destroyById(itemShelvingId, myOptions);
promises.push(itemShelvingToDelete);
}
const deletedItemShelvings = await Promise.all(promises);
if (tx) await tx.commit();
return deletedItemShelvings;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('ItemShelving deleteItemShelvings()', () => {
it('should return the deleted itemShelvings', async() => {
const tx = await models.Order.beginTransaction({});
try {
const options = {transaction: tx};
const itemShelvingIds = [1, 2];
const result = await models.ItemShelving.deleteItemShelvings(itemShelvingIds, options);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -53,6 +53,9 @@
"ItemShelvingSale": {
"dataSource": "vn"
},
"ItemShelvingPlacementSupplyStock": {
"dataSource": "vn"
},
"ItemImageQueue": {
"dataSource": "vn"
},

View File

@ -0,0 +1,36 @@
{
"name": "ItemShelvingPlacementSupplyStock",
"base": "VnModel",
"options": {
"mysql": {
"table": "itemShelvingPlacementSupplyStock"
}
},
"properties": {
"itemShelvingFk": {
"type": "number",
"id": true
},
"created": {
"type": "date"
},
"itemFk": {
"type": "number"
},
"longName": {
"type": "string"
},
"parking": {
"type": "string"
},
"shelving": {
"type": "string"
},
"packing": {
"type": "number"
},
"stock": {
"type": "number"
}
}
}

View File

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

View File

@ -23,9 +23,9 @@
</vn-portal>
<div class="vn-w-xl">
<vn-card>
<smart-table
<smart-table
model="model"
options="$ctrl.smartTableOptions"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-table>
<table>
@ -34,18 +34,18 @@
<th field="itemFk">
<span translate>Item ID</span>
</th>
<th field="itemName">
<th field="name">
<span translate>Description</span>
</th>
<th field="warehouseFk">
<span translate>Warehouse</span>
</th>
<th
<th
field="rate2"
vn-tooltip="Price By Unit">
<span translate>P.P.U.</span>
</th>
<th
<th
field="rate3"
vn-tooltip="Price By Package">
<span translate>P.P.P.</span>
@ -170,7 +170,7 @@
</vn-icon-button>
</div>
</slot-table>
</smart-table>
</smart-table>
</vn-card>
</div>
<vn-item-descriptor-popover
@ -182,4 +182,4 @@
on-accept="$ctrl.removePrice($data.$index)"
question="Are you sure you want to continue?"
message="This row will be removed">
</vn-confirm>
</vn-confirm>

View File

@ -12,14 +12,6 @@ export default class Controller extends Section {
},
defaultSearch: true,
columns: [
{
field: 'itemName',
autocomplete: {
url: 'Items',
showField: 'name',
valueField: 'id'
}
},
{
field: 'warehouseFk',
autocomplete: {
@ -105,8 +97,8 @@ export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
case 'itemName':
return {'i.id': value};
case 'name':
return {'i.name': {like: `%${value}%`}};
case 'itemFk':
case 'warehouseFk':
case 'rate2':

View File

@ -24,3 +24,5 @@ import './waste/detail';
import './fixed-price';
import './fixed-price-search-panel';
import './item-type';
import './item-shelving';

View File

@ -0,0 +1,118 @@
<vn-crud-model
vn-id="model"
url="ItemShelvingPlacementSupplyStocks"
link="{itemFk: $ctrl.$params.id}"
data="$ctrl.itemShelvingPlacementSupplyStocks"
auto-load="true">
</vn-crud-model>
<vn-card>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<div>
<div class="totalBox" style="text-align: center;">
<h6 translate>Total</h6>
<vn-label-value
label="Total labels"
value="{{$ctrl.labelTotal.toFixed(2)}}">
</vn-label-value>
</div>
</div>
<div class="vn-pa-md">
<vn-button
disabled="!$ctrl.checked.length"
ng-click="removeConfirm.show()"
icon="delete"
vn-tooltip="Remove selected lines"
vn-acl="replenisherBos">
</vn-button>
</div>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</th>
<th field="created">
<span translate>Created</span>
</th>
<th shrink field="itemFk">
<span translate>Item</span>
</th>
<th
field="longName">
<span translate>Concept</span>
</th>
<th
field="parking">
<span translate>Parking</span>
</th>
<th field="shelving">
<span translate>Shelving</span>
</th>
<th
field="label">
<span translate>Etiqueta</span>
</th>
<th
field="packing"
shrink>
<span translate>Packing</span>
</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="itemShelvingPlacementSupplyStock in $ctrl.itemShelvingPlacementSupplyStocks"
vn-repeat-last on-last="$ctrl.calculateTotals()">
<td shrink>
<vn-check
ng-model="itemShelvingPlacementSupplyStock.checked"
vn-click-stop>
</vn-check>
</td>
<td shrink-date>{{::itemShelvingPlacementSupplyStock.created | date: 'dd/MM/yyyy'}}</td>
<td>
{{::itemShelvingPlacementSupplyStock.itemFk}}
</td>
<td expand title="{{::itemShelvingPlacementSupplyStock.longName}}">
<span
vn-click-stop="itemDescriptor.show($event, itemShelvingPlacementSupplyStock.itemFk)"
class="link">
{{itemShelvingPlacementSupplyStock.longName}}
</span>
</td>
<td>
{{::itemShelvingPlacementSupplyStock.parking}}
</td>
<td>
{{::itemShelvingPlacementSupplyStock.shelving}}
</td>
<td>
{{(itemShelvingPlacementSupplyStock.stock / itemShelvingPlacementSupplyStock.packing).toFixed(2)}}
</td>
<td>
{{::itemShelvingPlacementSupplyStock.packing}}
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor">
</vn-item-descriptor-popover>
<vn-confirm
vn-id="removeConfirm"
message="Selected lines will be deleted"
question="Are you sure you want to continue?"
on-accept="$ctrl.onRemove()">
</vn-confirm>

View File

@ -0,0 +1,89 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.smartTableOptions = {
activeButtons: {
search: true
},
columns: [
{
field: 'parking',
autocomplete: {
url: 'Parkings',
showField: 'code',
valueField: 'code'
}
},
{
field: 'shelving',
autocomplete: {
url: 'Shelvings',
showField: 'code',
valueField: 'code'
}
},
{
field: 'created',
searchable: false
},
{
field: 'itemFk',
searchable: false
},
{
field: 'longName',
searchable: false
}
]
};
}
get checked() {
const itemShelvings = this.$.model.data || [];
const checkedLines = [];
for (let itemShelving of itemShelvings) {
if (itemShelving.checked)
checkedLines.push(itemShelving.itemShelvingFk);
}
return checkedLines;
}
calculateTotals() {
this.labelTotal = 0;
const itemShelvings = this.$.model.data || [];
itemShelvings.forEach(itemShelving => {
const label = itemShelving.stock / itemShelving.packing;
this.labelTotal += label;
});
}
onRemove() {
const params = {itemShelvingIds: this.checked};
const query = `ItemShelvings/deleteItemShelvings`;
this.$http.post(query, params)
.then(() => {
this.vnApp.showSuccess(this.$t('ItemShelvings removed'));
this.$.model.refresh();
});
}
exprBuilder(param, value) {
switch (param) {
case 'parking':
case 'shelving':
case 'label':
case 'packing':
return {[param]: value};
}
}
}
ngModule.vnComponent('vnItemShelving', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,81 @@
import './index';
import crudModel from 'core/mocks/crud-model';
describe('item shelving', () => {
describe('Component vnItemShelving', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('item'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-item-shelving></vn-item-shelving>');
controller = $componentController('vnItemShelving', {$element});
controller.$.model = crudModel;
controller.$.model.data = [
{itemShelvingFk: 1, packing: 10, stock: 1},
{itemShelvingFk: 2, packing: 12, stock: 5},
{itemShelvingFk: 4, packing: 20, stock: 10}
];
const modelData = controller.$.model.data;
modelData[0].checked = true;
modelData[1].checked = true;
}));
describe('checked() getter', () => {
it('should return a the selected rows', () => {
const result = controller.checked;
expect(result).toEqual(expect.arrayContaining([1, 2]));
});
});
describe('calculateTotals()', () => {
it('should calculate the total of labels', () => {
controller.calculateTotals();
expect(controller.labelTotal).toEqual(1.0166666666666666);
});
});
describe('onRemove()', () => {
it('shoud remove the selected lines', () => {
jest.spyOn(controller.$.model, 'refresh');
const expectedParams = {itemShelvingIds: [1, 2]};
$httpBackend.expectPOST('ItemShelvings/deleteItemShelvings', expectedParams).respond(200);
controller.onRemove();
$httpBackend.flush();
expect(controller.$.model.refresh).toHaveBeenCalled();
});
});
describe('exprBuilder()', () => {
it('should search by parking', () => {
const expr = controller.exprBuilder('parking', '700-01');
expect(expr).toEqual({'parking': '700-01'});
});
it('should search by shelving', () => {
const expr = controller.exprBuilder('shelving', 'AAA');
expect(expr).toEqual({'shelving': 'AAA'});
});
it('should search by label', () => {
const expr = controller.exprBuilder('label', 0.17);
expect(expr).toEqual({'label': 0.17});
});
it('should search by packing', () => {
const expr = controller.exprBuilder('packing', 10);
expect(expr).toEqual({'packing': 10});
});
});
});
});

View File

@ -0,0 +1,5 @@
Shelving: Matrícula
Remove selected lines: Eliminar líneas seleccionadas
Selected lines will be deleted: Las líneas seleccionadas serán eliminadas
ItemShelvings removed: Carros eliminados
Total labels: Total etiquetas

View File

@ -54,6 +54,7 @@ Basic data: Datos básicos
Tax: IVA
History: Historial
Botanical: Botánico
Shelvings: Carros
Barcodes: Códigos de barras
Diary: Histórico
Item diary: Registro de compra-venta

View File

@ -15,11 +15,12 @@
"card": [
{"state": "item.card.basicData", "icon": "settings"},
{"state": "item.card.tags", "icon": "icon-tags"},
{"state": "item.card.last-entries", "icon": "icon-regentry"},
{"state": "item.card.tax", "icon": "icon-tax"},
{"state": "item.card.botanical", "icon": "local_florist"},
{"state": "item.card.botanical", "icon": "local_florist"},
{"state": "item.card.shelving", "icon": "icon-inventory"},
{"state": "item.card.itemBarcode", "icon": "icon-barcode"},
{"state": "item.card.diary", "icon": "icon-transaction"},
{"state": "item.card.last-entries", "icon": "icon-regentry"},
{"state": "item.card.log", "icon": "history"}
],
"itemType": [
@ -92,6 +93,16 @@
},
"acl": ["buyer"]
},
{
"url" : "/shelving",
"state": "item.card.shelving",
"component": "vn-item-shelving",
"description": "Shelvings",
"params": {
"item": "$ctrl.item"
},
"acl": ["employee"]
},
{
"url" : "/barcode",
"state": "item.card.itemBarcode",

View File

@ -19,22 +19,24 @@
</vn-none>
</vn-horizontal>
<vn-card vn-id="card">
<smart-table
model="model"
options="$ctrl.smartTableOptions"
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"
disabled-table-filter="true"
disabled-table-order="true"
class="scrollable sm">
<slot-actions>
<vn-horizontal>
<vn-date-picker
class="vn-pa-xs"
label="From"
label="From"
ng-model="$ctrl.dateFrom"
on-change="$ctrl.addFilterDate()">
</vn-date-picker>
<vn-date-picker
class="vn-pa-xs"
label="To"
label="To"
ng-model="$ctrl.dateTo"
on-change="$ctrl.addFilterDate()">
</vn-date-picker>
@ -100,9 +102,9 @@
</slot-pagination>
</smart-table>
</vn-card>
<vn-worker-descriptor-popover
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
</vn-client-descriptor-popover>

View File

@ -39,7 +39,7 @@
"abstract": true,
"component": "vn-route-card"
}, {
"url": "/agency-term",
"url": "/agency-term?q",
"abstract": true,
"state": "route.agencyTerm",
"component": "ui-view"
@ -49,12 +49,12 @@
"component": "vn-agency-term-index",
"description": "Autonomous",
"acl": ["administrative"]
},{
},{
"url": "/createInvoiceIn?q",
"state": "route.agencyTerm.createInvoiceIn",
"component": "vn-agency-term-create-invoice-in",
"description": "File management",
"params": {
"params": {
"route": "$ctrl.route"
},
"acl": ["administrative"]
@ -92,4 +92,4 @@
"acl": ["delivery"]
}
]
}
}

View File

@ -13,9 +13,6 @@
{"state": "shelving.card.log", "icon": "history"}
]
},
"keybindings": [
{"key": "s", "state": "shelving.index"}
],
"routes": [
{
"url": "/shelving",

View File

@ -51,6 +51,9 @@
"isSerious": {
"type": "boolean"
},
"isTrucker": {
"type": "boolean"
},
"note": {
"type": "string"
},

View File

@ -41,6 +41,7 @@ class Controller extends Descriptor {
'payDay',
'isActive',
'isSerious',
'isTrucker',
'account'
],
include: [

View File

@ -27,6 +27,7 @@ describe('Supplier Component vnSupplierDescriptor', () => {
'payDay',
'isActive',
'isSerious',
'isTrucker',
'account'
],
include: [

View File

@ -118,8 +118,6 @@
rule
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Postcode"
ng-model="$ctrl.supplier.postCode"
@ -144,6 +142,8 @@
</vn-icon-button>
</append>
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-id="town" vn-one
label="City"
ng-model="$ctrl.supplier.city"
@ -159,8 +159,6 @@
({{province.country.country}})
</tpl-item>
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.supplier.provinceFk"
@ -172,6 +170,8 @@
rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="country" vn-one
ng-model="$ctrl.supplier.countryFk"
data="countries"
@ -180,6 +180,10 @@
label="Country"
rule>
</vn-autocomplete>
<vn-check
label="Trucker"
ng-model="$ctrl.supplier.isTrucker">
</vn-check>
</vn-horizontal>
</vn-card>
<vn-button-bar>

View File

@ -3,3 +3,4 @@ Sage transaction type: Tipo de transacción Sage
Sage withholding: Retención Sage
Supplier activity: Actividad proveedor
Healt register: Pasaporte sanitario
Trucker: Transportista

View File

@ -0,0 +1,52 @@
module.exports = Self => {
Self.remoteMethod('deleteExpeditions', {
description: 'Delete the selected expeditions',
accessType: 'WRITE',
accepts: [{
arg: 'expeditionIds',
type: ['number'],
required: true,
description: 'The expeditions ids to delete'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteExpeditions`,
verb: 'POST'
}
});
Self.deleteExpeditions = async(expeditionIds, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const promises = [];
for (let expeditionId of expeditionIds) {
const deletedExpedition = models.Expedition.destroyById(expeditionId, myOptions);
promises.push(deletedExpedition);
}
const deletedExpeditions = await Promise.all(promises);
if (tx) await tx.commit();
return deletedExpeditions;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,93 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('moveExpeditions', {
description: 'Move the selected expeditions to another ticket',
accessType: 'WRITE',
accepts: [{
arg: 'clientId',
type: 'number',
description: `The client id`,
required: true
},
{
arg: 'landed',
type: 'date',
description: `The landing date`
},
{
arg: 'warehouseId',
type: 'number',
description: `The warehouse id`,
required: true
},
{
arg: 'addressId',
type: 'number',
description: `The address id`,
required: true
},
{
arg: 'agencyModeId',
type: 'any',
description: `The agencyMode id`
},
{
arg: 'routeId',
type: 'any',
description: `The route id`
},
{
arg: 'expeditionIds',
type: ['number'],
required: true,
description: 'The expeditions ids to move'
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/moveExpeditions`,
verb: 'POST'
}
});
Self.moveExpeditions = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
if (args.routeId) {
const route = await models.Route.findById(args.routeId, null, myOptions);
if (!route) throw new UserError('This route does not exists');
}
const ticket = await models.Ticket.new(ctx, myOptions);
const promises = [];
for (let expeditionsId of args.expeditionIds) {
const expeditionToUpdate = await models.Expedition.findById(expeditionsId, null, myOptions);
const expeditionUpdated = expeditionToUpdate.updateAttribute('ticketFk', ticket.id, myOptions);
promises.push(expeditionUpdated);
}
await Promise.all(promises);
if (tx) await tx.commit();
return ticket;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,22 @@
const models = require('vn-loopback/server/server').models;
describe('ticket deleteExpeditions()', () => {
it('should delete the selected expeditions', async() => {
const tx = await models.Expedition.beginTransaction({});
try {
const options = {transaction: tx};
const expeditionIds = [12, 13];
const result = await models.Expedition.deleteExpeditions(expeditionIds, options);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,39 @@
const models = require('vn-loopback/server/server').models;
describe('ticket moveExpeditions()', () => {
it('should move the selected expeditions to new ticket', async() => {
const tx = await models.Expedition.beginTransaction({});
const ctx = {
req: {accessToken: {userId: 9}},
args: {},
params: {}
};
const myCtx = Object.assign({}, ctx);
try {
const options = {transaction: tx};
myCtx.args = {
clientId: 1101,
landed: new Date(),
warehouseId: 1,
addressId: 121,
agencyModeId: 1,
routeId: null,
expeditionIds: [1, 2]
};
const ticket = await models.Expedition.moveExpeditions(myCtx, options);
const newestTicketIdInFixtures = 27;
expect(ticket.id).toBeGreaterThan(newestTicketIdInFixtures);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -79,10 +79,18 @@ describe('sale updatePrice()', () => {
const price = 5.4;
const originalSalesPersonMana = await models.WorkerMana.findById(18, null, options);
const manaComponent = await models.Component.findOne({where: {code: 'mana'}}, options);
const teamOne = 96;
const userId = ctx.req.accessToken.userId;
const business = await models.Business.findOne({where: {workerFk: userId}}, options);
await business.updateAttribute('departmentFk', teamOne, options);
await models.Sale.updatePrice(ctx, saleId, price, options);
const updatedSale = await models.Sale.findById(saleId, null, options);
createdSaleComponent = await models.SaleComponent.findOne({where: {saleFk: saleId, componentFk: manaComponent.id}}, options);
const createdSaleComponent = await models.SaleComponent.findOne({
where: {
saleFk: saleId, componentFk: manaComponent.id
}}, options);
expect(updatedSale.price).toBe(price);
expect(createdSaleComponent.value).toEqual(-2.04);

View File

@ -0,0 +1,48 @@
const models = require('vn-loopback/server/server').models;
describe('sale usesMana()', () => {
const ctx = {
req: {
accessToken: {userId: 18}
}
};
it('should return that the worker uses mana', async() => {
const tx = await models.Sale.beginTransaction({});
try {
const options = {transaction: tx};
const teamOne = 96;
const userId = ctx.req.accessToken.userId;
const business = await models.Business.findOne({where: {workerFk: userId}}, options);
await business.updateAttribute('departmentFk', teamOne, options);
const usesMana = await models.Sale.usesMana(ctx, options);
expect(usesMana).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return that the worker not uses mana', async() => {
const tx = await models.Sale.beginTransaction({});
try {
const options = {transaction: tx};
const usesMana = await models.Sale.usesMana(ctx, options);
expect(usesMana).toBe(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -77,7 +77,8 @@ module.exports = Self => {
const oldPrice = sale.price;
const userId = ctx.req.accessToken.userId;
const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, myOptions);
const usesMana = await models.Sale.usesMana(ctx, myOptions);
const componentCode = usesMana ? 'mana' : 'buyerDiscount';
const discount = await models.Component.findOne({where: {code: componentCode}}, myOptions);
const componentId = discount.id;
@ -88,7 +89,6 @@ module.exports = Self => {
saleFk: id
};
const saleComponent = await models.SaleComponent.findOne({where}, myOptions);
if (saleComponent) {
await models.SaleComponent.updateAll(where, {
value: saleComponent.value + componentValue

View File

@ -0,0 +1,31 @@
module.exports = Self => {
Self.remoteMethodCtx('usesMana', {
description: 'Returns if the worker uses mana',
accessType: 'READ',
accepts: [],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/usesMana`,
verb: 'GET'
}
});
Self.usesMana = async(ctx, options) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const salesDepartment = await models.Department.findOne({where: {code: 'VT'}, fields: 'id'}, myOptions);
const departments = await models.Department.getLeaves(salesDepartment.id, null, myOptions);
const workerDepartment = await models.WorkerDepartment.findById(userId, null, myOptions);
const usesMana = departments.find(department => department.id == workerDepartment.departmentFk);
return usesMana ? true : false;
};
};

View File

@ -18,9 +18,12 @@ describe('ticket componentUpdate()', () => {
beforeAll(async() => {
const deliveryComponenet = await models.Component.findOne({where: {code: 'delivery'}});
deliveryComponentId = deliveryComponenet.id;
componentOfSaleSeven = `SELECT value FROM vn.saleComponent WHERE saleFk = 7 AND componentFk = ${deliveryComponentId}`;
componentOfSaleEight = `SELECT value FROM vn.saleComponent WHERE saleFk = 8 AND componentFk = ${deliveryComponentId}`;
componentOfSaleSeven = `SELECT value
FROM vn.saleComponent
WHERE saleFk = 7 AND componentFk = ${deliveryComponentId}`;
componentOfSaleEight = `SELECT value
FROM vn.saleComponent
WHERE saleFk = 8 AND componentFk = ${deliveryComponentId}`;
[componentValue] = await models.SaleComponent.rawSql(componentOfSaleSeven);
firstvalueBeforeChange = componentValue.value;

View File

@ -110,6 +110,11 @@ describe('sale updateDiscount()', () => {
const componentId = manaDiscount.id;
const manaCode = 'mana';
const teamOne = 96;
const userId = ctx.req.accessToken.userId;
const business = await models.Business.findOne({where: {workerFk: userId}}, options);
await business.updateAttribute('departmentFk', teamOne, options);
await models.Ticket.updateDiscount(ctx, ticketId, sales, newDiscount, manaCode, options);
const updatedSale = await models.Sale.findById(originalSaleId, null, options);
@ -150,6 +155,11 @@ describe('sale updateDiscount()', () => {
const componentId = manaDiscount.id;
const manaCode = 'manaClaim';
const teamOne = 96;
const userId = ctx.req.accessToken.userId;
const business = await models.Business.findOne({where: {workerFk: userId}}, options);
await business.updateAttribute('departmentFk', teamOne, options);
await models.Ticket.updateDiscount(ctx, ticketId, sales, newDiscount, manaCode, options);
const updatedSale = await models.Sale.findById(originalSaleId, null, options);

View File

@ -98,12 +98,7 @@ module.exports = Self => {
if (isLocked || (!hasAllowedRoles && alertLevel > 0))
throw new UserError(`The sales of this ticket can't be modified`);
const usesMana = await models.WorkerMana.findOne({
where: {
workerFk: userId
},
fields: 'amount'}, myOptions);
const usesMana = await models.Sale.usesMana(ctx, myOptions);
const componentCode = usesMana ? manaCode : 'buyerDiscount';
const discountComponent = await models.Component.findOne({
where: {code: componentCode}}, myOptions);
@ -115,14 +110,38 @@ module.exports = Self => {
for (let sale of sales) {
const oldDiscount = sale.discount;
const value = ((-sale.price * newDiscount) / 100);
const newComponent = models.SaleComponent.upsert({
saleFk: sale.id,
value: value,
componentFk: componentId}, myOptions);
const manaComponent = await models.Component.findOne({
where: {code: 'mana'}
}, myOptions);
const manaClaimComponent = await models.Component.findOne({
where: {code: 'manaClaim'}
}, myOptions);
const [oldComponent] = await models.SaleComponent.find({
where: {
and: [
{saleFk: sale.id},
{componentFk: {inq: [manaComponent.id, manaClaimComponent.id]}}
]
}
}, myOptions);
let deletedComponent;
if (oldComponent) {
const filter = {
saleFk: sale.id,
componentFk: oldComponent.componentFk
};
deletedComponent = await models.SaleComponent.destroyAll(filter, myOptions);
}
const newComponent = await createSaleComponent(sale.id, value, componentId, myOptions);
const updatedSale = sale.updateAttribute('discount', newDiscount, myOptions);
promises.push(newComponent, updatedSale);
promises.push(newComponent, updatedSale, deletedComponent);
const change = `${oldDiscount}% ➔ *${newDiscount}%*`;
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${change}`;
@ -165,4 +184,14 @@ module.exports = Self => {
throw e;
}
};
async function createSaleComponent(saleId, value, componentId, myOptions) {
const models = Self.app.models;
return models.SaleComponent.create({
saleFk: saleId,
value: value,
componentFk: componentId
}, myOptions);
}
};

View File

@ -1,3 +1,5 @@
module.exports = function(Self) {
require('../methods/expedition/filter')(Self);
require('../methods/expedition/deleteExpeditions')(Self);
require('../methods/expedition/moveExpeditions')(Self);
};

View File

@ -8,6 +8,7 @@ module.exports = Self => {
require('../methods/sale/recalculatePrice')(Self);
require('../methods/sale/refund')(Self);
require('../methods/sale/canEdit')(Self);
require('../methods/sale/usesMana')(Self);
Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank`

View File

@ -8,54 +8,77 @@
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-w-xl">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th></vn-th>
<vn-th field="itemFk" number>Expedition</vn-th>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="packageItemName">Name</vn-th>
<vn-th field="freightItemName">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="created" expand>Created</vn-th>
<vn-th field="state" expand>State</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="expedition in expeditions">
<vn-td class="vn-px-md" style="width:30px; color:#FFA410;">
<vn-icon-button icon="delete"
ng-click="deleteExpedition.show(expedition.id)"
vn-tooltip="Delete expedition">
</vn-icon-button>
</vn-td>
<vn-td number expand>{{expedition.id | zeroFill:6}}</vn-td>
<vn-td number>
<span
ng-class="{link: expedition.packagingItemFk}"
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)">
{{expedition.packagingFk}}
</span>
</vn-td>
<vn-td>{{::expedition.packageItemName}}</vn-td>
<vn-td>{{::expedition.freightItemName}}</vn-td>
<vn-td number>{{::expedition.counter}}</vn-td>
<vn-td expand>{{::expedition.externalId}}</vn-td>
<vn-td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td>{{::expedition.state}}</vn-td>
<vn-td>
<vn-icon-button
vn-click-stop="$ctrl.showLog(expedition)"
vn-tooltip="Status log"
icon="history">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<vn-card class="vn-pa-lg">
<vn-horizontal class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button icon="keyboard_arrow_down"
label="Move"
ng-click="moreOptions.show($event)"
disabled="!$ctrl.totalChecked">
</vn-button>
<vn-button
disabled="!$ctrl.checked.length"
ng-click="removeConfirm.show()"
icon="delete"
vn-tooltip="Delete expedition">
</vn-button>
</vn-tool-bar>
<vn-one class="taxes" ng-if="$ctrl.sales.length > 0">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}</strong></p>
</vn-one>
</vn-horizontal>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="itemFk" number>Expedition</vn-th>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="packageItemName">Name</vn-th>
<vn-th field="freightItemName">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="created" expand>Created</vn-th>
<vn-th field="state" expand>State</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="expedition in expeditions">
<vn-td shrink>
<vn-check tabindex="-1"
ng-model="expedition.checked">
</vn-check>
</vn-td>
<vn-td number expand>{{expedition.id | zeroFill:6}}</vn-td>
<vn-td number>
<span
ng-class="{link: expedition.packagingItemFk}"
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)">
{{expedition.packagingFk}}
</span>
</vn-td>
<vn-td>{{::expedition.packageItemName}}</vn-td>
<vn-td>{{::expedition.freightItemName}}</vn-td>
<vn-td number>{{::expedition.counter}}</vn-td>
<vn-td expand>{{::expedition.externalId}}</vn-td>
<vn-td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td>{{::expedition.state}}</vn-td>
<vn-td>
<vn-icon-button
vn-click-stop="$ctrl.showLog(expedition)"
vn-tooltip="Status log"
icon="history">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-item-descriptor-popover
@ -66,25 +89,25 @@
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>
<vn-confirm
vn-id="delete-expedition"
on-accept="$ctrl.onDialogAccept($data)"
<vn-confirm
vn-id="removeConfirm"
message="Are you sure you want to delete this expedition?"
question="Delete expedition"
message="Are you sure you want to delete this expedition?">
on-accept="$ctrl.onRemove()">
</vn-confirm>
<vn-popup vn-id="statusLog">
<vn-crud-model
vn-id="model"
vn-id="modelExpeditionStates"
url="ExpeditionStates/filter"
link="{expeditionFk: $ctrl.expedition.id}"
data="expeditionStates"
order="created DESC"
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-data-viewer model="modelExpeditionStates">
<vn-card class="vn-w-md">
<vn-table model="model">
<vn-table model="modelExpeditionStates">
<vn-thead>
<vn-tr>
<vn-th field="state">State</vn-th>
@ -111,4 +134,37 @@
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
</vn-popup>
</vn-popup>
<vn-menu vn-id="moreOptions">
<vn-item translate
name="withoutRoute"
ng-click="selectLanded.show('withoutRoute')">
New ticket without route
</vn-item>
<vn-item translate
name="withRoute"
ng-click="selectLanded.show('withRoute')">
New ticket with route
</vn-item>
</vn-menu>
<vn-dialog
vn-id="selectLanded"
on-accept="$ctrl.createTicket($ctrl.landed, $ctrl.newRoute)">
<tpl-body>
<vn-date-picker
label="Landed"
ng-model="$ctrl.landed">
</vn-date-picker>
<vn-textfield
ng-show="selectLanded.data == 'withRoute'"
label="Route id"
ng-model="$ctrl.newRoute">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>

View File

@ -2,6 +2,27 @@ import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $scope) {
super($element, $scope);
this.landed = new Date();
this.newRoute = null;
}
get checked() {
const rows = this.$.model.data || [];
const checkedRows = [];
for (let row of rows) {
if (row.checked)
checkedRows.push(row.id);
}
return checkedRows;
}
get totalChecked() {
return this.checked.length;
}
onDialogAccept(id) {
return this.$http.delete(`Expeditions/${id}`)
.then(() => this.$.model.refresh());
@ -11,6 +32,33 @@ class Controller extends Section {
this.expedition = expedition;
this.$.statusLog.show();
}
onRemove() {
const params = {expeditionIds: this.checked};
const query = `Expeditions/deleteExpeditions`;
this.$http.post(query, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Expedition removed'));
this.$.model.refresh();
});
}
createTicket(landed, routeFk) {
const params = {
clientId: this.ticket.clientFk,
landed: landed,
warehouseId: this.ticket.warehouseFk,
addressId: this.ticket.addressFk,
agencyModeId: this.ticket.agencyModeFk,
routeId: routeFk,
expeditionIds: this.checked
};
const query = `Expeditions/moveExpeditions`;
this.$http.post(query, params).then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$state.go('ticket.card.summary', {id: res.data.id});
});
}
}
ngModule.vnComponent('vnTicketExpedition', {

View File

@ -17,6 +17,14 @@ describe('Ticket', () => {
refresh: () => {}
};
controller = $componentController('vnTicketExpedition', {$element: null, $scope});
controller.$.model.data = [
{id: 1},
{id: 2},
{id: 3}
];
const modelData = controller.$.model.data;
modelData[0].checked = true;
modelData[1].checked = true;
}));
describe('onDialogAccept()', () => {
@ -50,5 +58,51 @@ describe('Ticket', () => {
expect(controller.$.statusLog.show).toHaveBeenCalledWith();
});
});
describe('onRemove()', () => {
it('should make a query and then call to the model refresh() method', () => {
jest.spyOn($scope.model, 'refresh');
const expectedParams = {expeditionIds: [1, 2]};
$httpBackend.expect('POST', 'Expeditions/deleteExpeditions', expectedParams).respond(200);
controller.onRemove();
$httpBackend.flush();
expect($scope.model.refresh).toHaveBeenCalledWith();
});
});
describe('createTicket()', () => {
it('should make a query and then call to the $state go() method', () => {
jest.spyOn(controller.$state, 'go').mockReturnThis();
const ticket = {
clientFk: 1101,
landed: new Date(),
addressFk: 121,
agencyModeFk: 1,
warehouseFk: 1
};
const routeId = null;
controller.ticket = ticket;
const ticketToTransfer = {id: 28};
const expectedParams = {
clientId: 1101,
landed: new Date(),
warehouseId: 1,
addressId: 121,
agencyModeId: 1,
routeId: null,
expeditionIds: [1, 2]
};
$httpBackend.expect('POST', 'Expeditions/moveExpeditions', expectedParams).respond(ticketToTransfer);
controller.createTicket(ticket.landed, routeId);
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.summary', {id: ticketToTransfer.id});
});
});
});
});

View File

@ -1 +1,6 @@
Status log: Hitorial de estados
Status log: Hitorial de estados
Expedition removed: Expedición eliminada
Move: Mover
New ticket without route: Nuevo ticket sin ruta
New ticket with route: Nuevo ticket con ruta
Route id: Id ruta

View File

@ -244,7 +244,7 @@
</vn-spinner>
<div ng-if="$ctrl.edit.mana != null">
<section class="header vn-pa-md">
<h5>MANÁ: {{::$ctrl.edit.mana | currency: 'EUR': 0}}</h5>
<h5>Mana: {{::$ctrl.edit.mana | currency: 'EUR': 0}}</h5>
</section>
<div class="vn-pa-md">
<vn-input-number
@ -264,50 +264,6 @@
</div>
</div>
</div>
</div>
</vn-popover>
<!-- Discount popover -->
<vn-popover
vn-id="editDiscount"
on-open="$ctrl.getMana()">
<div class="edit-popover">
<vn-spinner class="vn-pa-xs"
ng-if="$ctrl.edit.mana == null"
enable="true">
</vn-spinner>
<div ng-if="$ctrl.edit.mana != null">
<section class="header vn-pa-md">
<h5>Mana: {{::$ctrl.edit.mana | currency: 'EUR':0}}</h5>
</section>
<div class="vn-pa-md">
<vn-input-number
vn-focus
label="Discount"
ng-model="$ctrl.edit.discount"
clear-disabled="true"
suffix="%">
</vn-input-number>
<vn-vertical ng-if="$ctrl.currentWorkerMana != 0">
<vn-radio
label="Promotion mana"
val="mana"
ng-model="$ctrl.manaCode">
</vn-radio>
<vn-radio
label="Claim mana"
val="manaClaim"
ng-model="$ctrl.manaCode">
</vn-radio>
</vn-vertical>
<div class="simulator">
<p class="simulatorTitle" translate>New price</p>
<p>
<strong>{{$ctrl.getNewPrice() | currency: 'EUR': 2}}</strong>
</p>
</div>
</div>
</div>
<vn-horizontal >
<vn-button
label="Cancel"
@ -321,31 +277,59 @@
</div>
</vn-popover>
<!-- Multiple discount dialog -->
<vn-dialog vn-id="editDiscountDialog"
on-open="$ctrl.getMana()"
message="Edit discount">
<tpl-body>
<!-- Discount popover -->
<vn-popover
vn-id="editDiscount"
on-open="$ctrl.getMana()">
<div class="edit-popover">
<vn-spinner class="vn-pa-xs"
ng-if="$ctrl.edit.mana == null"
enable="true">
</vn-spinner>
<div ng-if="$ctrl.edit.mana != null">
<section class="header vn-pa-md">
<h5>Mana: {{::$ctrl.edit.mana | currency: 'EUR': 0}}</h5>
</section>
<div class="vn-pa-md">
<vn-input-number vn-focus
<vn-input-number
vn-focus
label="Discount"
ng-model="$ctrl.edit.discount"
on-change="$ctrl.changeMultipleDiscount()"
clear-disabled="true"
suffix="%">
</vn-input-number>
<vn-vertical ng-if="$ctrl.usesMana && $ctrl.currentWorkerMana != 0">
<vn-radio
label="Promotion mana"
val="mana"
ng-model="$ctrl.manaCode">
</vn-radio>
<vn-radio
label="Claim mana"
val="manaClaim"
ng-model="$ctrl.manaCode">
</vn-radio>
</vn-vertical>
<div class="simulator" ng-show="$ctrl.edit.sale">
<p class="simulatorTitle" translate>New price</p>
<p>
<strong>{{$ctrl.getNewPrice() | currency: 'EUR': 2}}</strong>
</p>
</div>
</div>
<section class="header vn-pa-md">
<span>Mana: <strong>{{::$ctrl.edit.mana | currency: 'EUR': 0}}</strong></span>
</section>
</div>
</tpl-body>
</vn-dialog>
<vn-horizontal >
<vn-button
label="Cancel"
ng-click="$ctrl.cancel()">
</vn-button>
<vn-button
label="Save"
ng-click="$ctrl.save()">
</vn-button>
</vn-horizontal>
</div>
</vn-popover>
<!-- Transfer Popover -->
<vn-popover vn-id="transfer">
@ -490,7 +474,7 @@
</vn-item>
<vn-item translate
name="discount"
ng-click="$ctrl.showEditDiscountDialog($event)">
ng-click="$ctrl.showEditDiscountPopover($event, sale)">
Update discount
</vn-item>
<vn-item translate

View File

@ -75,6 +75,7 @@ class Controller extends Section {
this.$.editPricePopover.relocate();
});
});
this.getUsesMana();
this.getCurrentWorkerMana();
}
@ -85,6 +86,13 @@ class Controller extends Section {
});
}
getUsesMana() {
this.$http.get(`Sales/usesMana`)
.then(res => {
this.useMana = res.data;
});
}
/**
* Returns checked instances
*
@ -243,26 +251,21 @@ class Controller extends Section {
showEditDiscountPopover(event, sale) {
if (this.isLocked) return;
this.edit = {
discount: sale.discount,
sale: sale
};
if (sale) {
this.edit = {
discount: sale.discount,
sale: sale
};
} else {
this.edit = {
discount: null,
sales: this.selectedValidSales()
};
}
this.$.editDiscount.show(event);
}
showEditDiscountDialog(event) {
if (this.isLocked) return;
this.edit = {
discount: null,
sales: this.selectedValidSales()
};
this.$.editDiscountDialog.show(event);
}
changeDiscount() {
const sale = this.edit.sale;
const newDiscount = this.edit.discount;
@ -278,11 +281,10 @@ class Controller extends Section {
const hasChanges = sales.some(sale => {
return sale.discount != newDiscount;
});
if (newDiscount != null && hasChanges)
this.updateDiscount(sales);
this.$.editDiscountDialog.hide();
this.$.editDiscount.hide();
}
updateDiscount(sales) {
@ -303,7 +305,7 @@ class Controller extends Section {
}
getNewPrice() {
if (this.edit) {
if (this.edit.sale) {
const sale = this.edit.sale;
let newDiscount = sale.discount;
let newPrice = this.edit.price || sale.price;
@ -505,7 +507,8 @@ class Controller extends Section {
}
save() {
this.changeDiscount();
if (this.edit.sale) this.changeDiscount();
if (this.edit.sales) this.changeMultipleDiscount();
}
cancel() {

View File

@ -115,6 +115,7 @@ describe('Ticket', () => {
const expectedAmount = 250;
$httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount);
$httpBackend.expect('GET', 'Sales/usesMana').respond(200);
$httpBackend.expect('GET', 'WorkerManas/getCurrentWorkerMana').respond(200, expectedAmount);
controller.getMana();
$httpBackend.flush();
@ -393,7 +394,7 @@ describe('Ticket', () => {
secondSelectedSale.checked = true;
const expectedSales = [firstSelectedSale, secondSelectedSale];
controller.$.editDiscountDialog = {hide: jest.fn()};
controller.$.editDiscount = {hide: jest.fn()};
controller.edit = {
discount: 10,
sales: expectedSales
@ -402,7 +403,7 @@ describe('Ticket', () => {
controller.changeMultipleDiscount();
expect(controller.updateDiscount).toHaveBeenCalledWith(expectedSales);
expect(controller.$.editDiscountDialog.hide).toHaveBeenCalledWith();
expect(controller.$.editDiscount.hide).toHaveBeenCalledWith();
});
it('should not call to the updateDiscount() method and then to the editDiscountDialog hide() method', () => {
@ -417,7 +418,7 @@ describe('Ticket', () => {
secondSelectedSale.discount = 10;
const expectedSales = [firstSelectedSale, secondSelectedSale];
controller.$.editDiscountDialog = {hide: jest.fn()};
controller.$.editDiscount = {hide: jest.fn()};
controller.edit = {
discount: 10,
sales: expectedSales
@ -426,7 +427,7 @@ describe('Ticket', () => {
controller.changeMultipleDiscount();
expect(controller.updateDiscount).not.toHaveBeenCalledWith(expectedSales);
expect(controller.$.editDiscountDialog.hide).toHaveBeenCalledWith();
expect(controller.$.editDiscount.hide).toHaveBeenCalledWith();
});
});

View File

@ -7,4 +7,4 @@ copyLink: 'Como alternativa, podes copiar o siguinte link no teu navegador:'
poll: Si o deseja, podes responder nosso questionário de satiscação para ajudar-nos a prestar-vos um melhor serviço. Tua opinião é muito importante para nós!
help: Cualquer dúvida que surja, no hesites em consultar-la, <strong>Estamos aqui para
atender-te!</strong>
conclusion: Obrigado por tua atenção!
conclusion: Obrigado por tua atenção!

Some files were not shown because too many files have changed in this diff Show More