Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 3256-order_transactions

This commit is contained in:
Carlos Jimenez Ruiz 2021-11-24 11:22:16 +01:00
commit c3da93bfd3
203 changed files with 3668 additions and 1383 deletions

View File

@ -27,7 +27,7 @@ module.exports = Self => {
Self.sendCheckingPresence = async(ctx, recipientId, message, options) => { Self.sendCheckingPresence = async(ctx, recipientId, message, options) => {
if (!recipientId) return false; if (!recipientId) return false;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -60,7 +60,7 @@ module.exports = Self => {
const args = ctx.args; const args = ctx.args;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -54,7 +54,7 @@ module.exports = Self => {
const args = ctx.args; const args = ctx.args;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -16,7 +16,7 @@ module.exports = function(Self) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -31,7 +31,7 @@ module.exports = function(Self) {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -23,7 +23,7 @@ module.exports = function(Self) {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -7,8 +7,7 @@ module.exports = function(Self) {
required: true, required: true,
description: `Code of the table you ask its configuration`, description: `Code of the table you ask its configuration`,
http: {source: 'body'} http: {source: 'body'}
} }],
],
returns: { returns: {
type: 'object', type: 'object',
root: true root: true
@ -29,6 +28,6 @@ module.exports = function(Self) {
config.userFk = ctx.req.accessToken.userId; config.userFk = ctx.req.accessToken.userId;
return await Self.app.models.UserConfigView.create(config); return Self.app.models.UserConfigView.create(config);
}; };
}; };

View File

@ -29,6 +29,9 @@
"ChatConfig": { "ChatConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },
"DefaultViewConfig": {
"dataSource": "vn"
},
"Delivery": { "Delivery": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,25 @@
{
"name": "DefaultViewConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "salix.defaultViewConfig"
}
},
"properties": {
"tableCode": {
"id": true,
"type": "string",
"required": true
},
"columns": {
"type": "object"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1,4 @@
ALTER TABLE vn.payMethod CHANGE ibanRequired ibanRequiredForClients tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod ADD ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod CHANGE ibanRequiredForSuppliers ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL AFTER ibanRequiredForClients;
UPDATE vn.payMethod SET ibanRequiredForSuppliers = 1 WHERE code = 'wireTransfer';

View File

@ -0,0 +1,14 @@
CREATE TABLE `salix`.`defaultViewConfig`
(
tableCode VARCHAR(25) not null,
columns JSON not null
)
comment 'The default configuration of columns for views';
INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns)
VALUES
('itemsIndex', '{"intrastat":false,"stemMultiplier":false,"landed":false}'),
('latestBuys', '{"intrastat":false,"description":false,"density":false,"isActive":false,"freightValue":false,"packageValue":false,"isIgnored":false,"price2":false,"minPrice":true,"ektFk":false,"weight":false,"id":true,"packing":true,"grouping":true,"quantity":true,"size":false,"name":true,"code":true,"origin":true,"family":true,"entryFk":true,"buyingValue":true,"comissionValue":false,"price3":true,"packageFk":true,"packingOut":true}'),
('ticketsMonitor', '{"id":false}');

View File

@ -0,0 +1,5 @@
UPDATE `salix`.`defaultViewConfig`
SET `columns` = '{"intrastat":false,"description":false,"density":false,"isActive":false,
"freightValue":false,"packageValue":false,"isIgnored":false,"price2":false,"ektFk":false,"weight":false,
"size":false,"comissionValue":false,"landing":false}'
WHERE tableCode = 'latestBuys'

View File

@ -1,9 +1,8 @@
DROP PROCEDURE IF EXISTS vn.item_getBalance; DROP PROCEDURE IF EXISTS `vn`.`item_getBalance`;
DELIMITER $$ DELIMITER $$
$$ $$
CREATE CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`item_getBalance`(IN vItemId int, IN vWarehouse int)
definer = root@`%` procedure vn.item_getBalance(IN vItemId int, IN vWarehouse int)
BEGIN BEGIN
DECLARE vDateInventory DATETIME; DECLARE vDateInventory DATETIME;
DECLARE vCurdate DATE DEFAULT CURDATE(); DECLARE vCurdate DATE DEFAULT CURDATE();
@ -116,7 +115,7 @@ BEGIN
s.id, s.id,
st.`order`, st.`order`,
ct.code, ct.code,
cl.id cb.claimFk
FROM sale s FROM sale s
JOIN ticket t ON t.id = s.ticketFk JOIN ticket t ON t.id = s.ticketFk
LEFT JOIN ticketState ts ON ts.ticket = t.id LEFT JOIN ticketState ts ON ts.ticket = t.id
@ -132,6 +131,7 @@ BEGIN
LEFT JOIN state stPrep ON stPrep.`code` = 'PREPARED' LEFT JOIN state stPrep ON stPrep.`code` = 'PREPARED'
LEFT JOIN saleTracking stk ON stk.saleFk = s.id AND stk.stateFk = stPrep.id LEFT JOIN saleTracking stk ON stk.saleFk = s.id AND stk.stateFk = stPrep.id
LEFT JOIN claim cl ON cl.ticketFk = t.id LEFT JOIN claim cl ON cl.ticketFk = t.id
LEFT JOIN claimBeginning cb ON cl.id = cb.claimFk AND s.id = cb.saleFk
WHERE t.shipped >= vDateInventory WHERE t.shipped >= vDateInventory
AND s.itemFk = vItemId AND s.itemFk = vItemId
AND vWarehouse =t.warehouseFk AND vWarehouse =t.warehouseFk
@ -141,4 +141,3 @@ BEGIN
END; END;
$$ $$
DELIMITER ; DELIMITER ;

View File

@ -0,0 +1,2 @@
ALTER TABLE vn.payMethod CHANGE ibanRequiredForClients isIbanRequiredForClients tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod CHANGE ibanRequiredForSuppliers isIbanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL;

View File

@ -217,14 +217,14 @@ UPDATE `vn`.`agencyMode` SET `web` = 1, `reportMail` = 'no-reply@gothamcity.com'
UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23; UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23;
INSERT INTO `vn`.`payMethod`(`id`,`code`, `name`, `graceDays`, `outstandingDebt`, `ibanRequired`) INSERT INTO `vn`.`payMethod`(`id`,`code`, `name`, `graceDays`, `outstandingDebt`, `isIbanRequiredForClients`, `isIbanRequiredForSuppliers`)
VALUES VALUES
(1, NULL, 'PayMethod one', 0, 001, 0), (1, NULL, 'PayMethod one', 0, 001, 0, 0),
(2, NULL, 'PayMethod two', 10, 001, 0), (2, NULL, 'PayMethod two', 10, 001, 0, 0),
(3, 'compensation', 'PayMethod three', 0, 001, 0), (3, 'compensation', 'PayMethod three', 0, 001, 0, 0),
(4, NULL, 'PayMethod with IBAN', 0, 001, 1), (4, NULL, 'PayMethod with IBAN', 0, 001, 1, 0),
(5, NULL, 'PayMethod five', 10, 001, 0), (5, NULL, 'PayMethod five', 10, 001, 0, 0),
(8,'wireTransfer', 'WireTransfer', 5, 001, 1); (8,'wireTransfer', 'WireTransfer', 5, 001, 1, 1);
INSERT INTO `vn`.`payDem`(`id`, `payDem`) INSERT INTO `vn`.`payDem`(`id`, `payDem`)
VALUES VALUES
@ -739,7 +739,7 @@ INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `warehouseFk`, `
(3, 'WPN', 'Paniculata', 2, 1, 31, 35, 0), (3, 'WPN', 'Paniculata', 2, 1, 31, 35, 0),
(4, 'PRT', 'Delivery ports', 3, 1, NULL, 35, 1), (4, 'PRT', 'Delivery ports', 3, 1, NULL, 35, 1),
(5, 'CON', 'Container', 3, 1, NULL, 35, 1), (5, 'CON', 'Container', 3, 1, NULL, 35, 1),
(6, 'ALS', 'Alstroemeria', 1, 1, 31, 35, 0); (6, 'ALS', 'Alstroemeria', 1, 1, 31, 16, 0);
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`) INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`)
VALUES VALUES
@ -796,25 +796,25 @@ INSERT INTO `vn`.`itemFamily`(`code`, `description`)
('SER', 'Services'), ('SER', 'Services'),
('VT', 'Sales'); ('VT', 'Sales');
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`, `comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`) INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`, `comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`)
VALUES VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT'), (1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT'), (2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT'), (3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT'), (4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT'), (5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT', 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT'), (6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT', 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT'), (7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT', 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT'), (8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT', 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT'), (9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT', 1),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT'), (10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT', 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT'), (11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT', 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT'), (12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT', 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 0, 2, 'VT'), (13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 0, 2, 'VT', 1),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT'), (14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT', 1),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB'), (15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB'), (16, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT'); (71, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT', 0);
-- Update the taxClass after insert of the items -- Update the taxClass after insert of the items
UPDATE `vn`.`itemTaxCountry` SET `taxClassFk` = 2 UPDATE `vn`.`itemTaxCountry` SET `taxClassFk` = 2
@ -1882,6 +1882,7 @@ INSERT INTO `postgresql`.`calendar_state` (`calendar_state_id`, `type`, `rgb`, `
INSERT INTO `postgresql`.`calendar_employee` (`business_id`, `calendar_state_id`, `date`) INSERT INTO `postgresql`.`calendar_employee` (`business_id`, `calendar_state_id`, `date`)
VALUES VALUES
(1, 6, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -10 DAY), DATE_ADD(CURDATE(), INTERVAL 10 DAY))),
(1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -10 DAY), DATE_ADD(CURDATE(), INTERVAL 10 DAY))), (1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -10 DAY), DATE_ADD(CURDATE(), INTERVAL 10 DAY))),
(1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -11 DAY), DATE_ADD(CURDATE(), INTERVAL 11 DAY))), (1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -11 DAY), DATE_ADD(CURDATE(), INTERVAL 11 DAY))),
(1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -12 DAY), DATE_ADD(CURDATE(), INTERVAL 12 DAY))), (1106, 1, IF(MONTH(CURDATE()) = 12 AND DAY(CURDATE()) > 10, DATE_ADD(CURDATE(), INTERVAL -12 DAY), DATE_ADD(CURDATE(), INTERVAL 12 DAY))),

View File

@ -33928,7 +33928,8 @@ CREATE TABLE `payMethod` (
`solution` varchar(1) COLLATE utf8_unicode_ci DEFAULT NULL, `solution` varchar(1) COLLATE utf8_unicode_ci DEFAULT NULL,
`outstandingDebt` tinyint(3) unsigned zerofill NOT NULL DEFAULT '000', `outstandingDebt` tinyint(3) unsigned zerofill NOT NULL DEFAULT '000',
`graceDays` int(11) unsigned NOT NULL DEFAULT '0', `graceDays` int(11) unsigned NOT NULL DEFAULT '0',
`ibanRequired` tinyint(3) DEFAULT '0', `ibanRequiredForClients` tinyint(3) DEFAULT '0',
`ibanRequiredForSuppliers` tinyint(3) DEFAULT '0',
`isNotified` tinyint(3) NOT NULL DEFAULT '1', `isNotified` tinyint(3) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; ) ENGINE=InnoDBDEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

@ -47,19 +47,20 @@ TABLES=(
cplusSubjectOp cplusSubjectOp
cplusTaxBreak cplusTaxBreak
cplusTrascendency472 cplusTrascendency472
pgc
time
claimResponsible claimResponsible
claimReason claimReason
claimRedelivery claimRedelivery
claimResult claimResult
ticketUpdateAction
state
sample
department
component component
componentType componentType
continent continent
department
itemPackingType
pgc
sample
state
ticketUpdateAction
time
volumeConfig volumeConfig
) )
dump_tables ${TABLES[@]} dump_tables ${TABLES[@]}

View File

@ -341,48 +341,32 @@ let actions = {
}, },
waitForTextInElement: async function(selector, text) { waitForTextInElement: async function(selector, text) {
const expectedText = text.toLowerCase(); await this.waitForFunction((selector, text) => {
return new Promise((resolve, reject) => { if (document.querySelector(selector)) {
let attempts = 0; const innerText = document.querySelector(selector).innerText.toLowerCase();
const interval = setInterval(async() => { const expectedText = text.toLowerCase();
const currentText = await this.evaluate(selector => { if (innerText.includes(expectedText))
return document.querySelector(selector).innerText.toLowerCase(); return innerText;
}, selector); }
}, {}, selector, text);
if (currentText === expectedText || attempts === 40) {
clearInterval(interval);
resolve(currentText);
}
attempts += 1;
}, 100);
}).then(result => {
return expect(result).toContain(expectedText);
});
}, },
waitForTextInField: async function(selector, text) { waitForTextInField: async function(selector, text) {
let builtSelector = await this.selectorFormater(selector); const builtSelector = await this.selectorFormater(selector);
await this.waitForSelector(builtSelector); const expectedValue = text.toLowerCase();
const expectedText = text.toLowerCase();
return new Promise((resolve, reject) => {
let attempts = 0;
const interval = setInterval(async() => {
const currentText = await this.evaluate(selector => {
return document.querySelector(selector).value.toLowerCase();
}, builtSelector);
if (currentText === expectedText || attempts === 40) { try {
clearInterval(interval); await this.waitForFunction((selector, text) => {
resolve(currentText); const element = document.querySelector(selector);
if (element) {
const value = element.value.toLowerCase();
if (value.includes(text))
return true;
} }
attempts += 1; }, {}, builtSelector, expectedValue);
}, 100); } catch (error) {
}).then(result => { throw new Error(`${text} wasn't the value of ${builtSelector}, ${error}`);
if (result === '') }
return expect(result).toEqual(expectedText);
return expect(result).toContain(expectedText);
});
}, },
selectorFormater: function(selector) { selectorFormater: function(selector) {

View File

@ -119,6 +119,7 @@ export default {
name: 'vn-client-create vn-textfield[ng-model="$ctrl.client.name"]', name: 'vn-client-create vn-textfield[ng-model="$ctrl.client.name"]',
taxNumber: 'vn-client-create vn-textfield[ng-model="$ctrl.client.fi"]', taxNumber: 'vn-client-create vn-textfield[ng-model="$ctrl.client.fi"]',
socialName: 'vn-client-create vn-textfield[ng-model="$ctrl.client.socialName"]', socialName: 'vn-client-create vn-textfield[ng-model="$ctrl.client.socialName"]',
businessType: 'vn-client-create vn-autocomplete[ng-model="$ctrl.client.businessTypeFk"]',
street: 'vn-client-create vn-textfield[ng-model="$ctrl.client.street"]', street: 'vn-client-create vn-textfield[ng-model="$ctrl.client.street"]',
addPostCode: 'vn-client-create vn-datalist[ng-model="$ctrl.client.postcode"] vn-icon-button[icon="add_circle"]', addPostCode: 'vn-client-create vn-datalist[ng-model="$ctrl.client.postcode"] vn-icon-button[icon="add_circle"]',
addProvince: 'vn-autocomplete[ng-model="$ctrl.location.provinceFk"] vn-icon-button[icon="add_circle"]', addProvince: 'vn-autocomplete[ng-model="$ctrl.location.provinceFk"] vn-icon-button[icon="add_circle"]',
@ -312,27 +313,26 @@ export default {
}, },
itemsIndex: { itemsIndex: {
createItemButton: `vn-float-button`, createItemButton: `vn-float-button`,
firstSearchResult: 'vn-item-index a:nth-child(1)', firstSearchResult: 'vn-item-index tbody tr:nth-child(1)',
searchResult: 'vn-item-index a.vn-tr', searchResult: 'vn-item-index tbody tr:not(.empty-rows)',
firstResultPreviewButton: 'vn-item-index vn-tbody > :nth-child(1) .buttons > [icon="preview"]', firstResultPreviewButton: 'vn-item-index tbody > :nth-child(1) .buttons > [icon="preview"]',
searchResultCloneButton: 'vn-item-index .buttons > [icon="icon-clone"]', searchResultCloneButton: 'vn-item-index .buttons > [icon="icon-clone"]',
acceptClonationAlertButton: '.vn-confirm.shown [response="accept"]', acceptClonationAlertButton: '.vn-confirm.shown [response="accept"]',
closeItemSummaryPreview: '.vn-popup.shown', closeItemSummaryPreview: '.vn-popup.shown',
fieldsToShowButton: 'vn-item-index vn-table > div > div > vn-icon-button[icon="more_vert"]', shownColumns: 'vn-item-index vn-button[id="shownColumns"]',
fieldsToShowForm: '.vn-popover.shown .content', shownColumnsList: '.vn-popover.shown .content',
firstItemImage: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > img', firstItemImage: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(1) > img',
firstItemImageTd: 'vn-item-index vn-table a:nth-child(1) vn-td:nth-child(1)', firstItemImageTd: 'vn-item-index smart-table tr:nth-child(1) td:nth-child(1)',
firstItemId: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(2)', firstItemId: 'vn-item-index tbody > tr:nth-child(1) > td:nth-child(2)',
idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(1) > vn-check', idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Identifier"]',
stemsCheckbox: '.vn-popover.shown vn-horizontal:nth-child(2) > vn-check', stemsCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Stems"]',
sizeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check', sizeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Size"]',
typeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(5) > vn-check', typeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Type"]',
categoryCheckbox: '.vn-popover.shown vn-horizontal:nth-child(6) > vn-check', categoryCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Category"]',
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(7) > vn-check', intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(8) > vn-check', originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(9) > vn-check', buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
destinyCheckbox: '.vn-popover.shown vn-horizontal:nth-child(10) > vn-check', densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]',
taxClassCheckbox: '.vn-popover.shown vn-horizontal:nth-child(11) > vn-check',
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button' saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
}, },
itemFixedPrice: { itemFixedPrice: {
@ -1086,7 +1086,7 @@ export default {
allBuyCheckbox: 'vn-entry-buy-index thead vn-check', allBuyCheckbox: 'vn-entry-buy-index thead vn-check',
firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check', firstBuyCheckbox: 'vn-entry-buy-index tbody:nth-child(2) vn-check',
deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]', deleteBuysButton: 'vn-entry-buy-index vn-button[icon="delete"]',
addBuyButton: 'vn-entry-buy-index vn-icon[icon="add_circle"]', addBuyButton: 'vn-entry-buy-index vn-icon[icon="add"]',
secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]', secondBuyPackingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price3"]',
secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]', secondBuyGroupingPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.price2"]',
secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]', secondBuyPrice: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.buyingValue"]',
@ -1108,9 +1108,9 @@ export default {
importBuysButton: 'vn-entry-buy-import button[type="submit"]' importBuysButton: 'vn-entry-buy-import button[type="submit"]'
}, },
entryLatestBuys: { entryLatestBuys: {
firstBuy: 'vn-entry-latest-buys vn-tbody > a:nth-child(1)', firstBuy: 'vn-entry-latest-buys tbody > tr:nth-child(1)',
allBuysCheckBox: 'vn-entry-latest-buys vn-thead vn-check', allBuysCheckBox: 'vn-entry-latest-buys thead vn-check',
secondBuyCheckBox: 'vn-entry-latest-buys a:nth-child(2) vn-check[ng-model="buy.checked"]', secondBuyCheckBox: 'vn-entry-latest-buys tbody tr:nth-child(2) vn-check[ng-model="buy.$checked"]',
editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]', editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]',
fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]', fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]',
newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]', newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]',

View File

@ -19,7 +19,9 @@ describe('Login path', async() => {
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
const state = await page.getState(); const state = await page.getState();
expect(message.text).toContain('Invalid login, remember that distinction is made between uppercase and lowercase'); const errorMessage = 'Invalid login, remember that distinction is made between uppercase and lowercase';
expect(message.text).toContain(errorMessage);
expect(state).toBe('login'); expect(state).toBe('login');
}); });
@ -28,7 +30,9 @@ describe('Login path', async() => {
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
const state = await page.getState(); const state = await page.getState();
expect(message.text).toContain('Invalid login, remember that distinction is made between uppercase and lowercase'); const errorMessage = 'Invalid login, remember that distinction is made between uppercase and lowercase';
expect(message.text).toContain(errorMessage);
expect(state).toBe('login'); expect(state).toBe('login');
}); });

View File

@ -27,16 +27,19 @@ describe('Client create path', () => {
await page.waitForState('client.create'); await page.waitForState('client.create');
}); });
it('should receive an error when clicking the create button having name and Business name fields empty', async() => { it('should receive an error when clicking the create button having name and Business name fields empty',
await page.write(selectors.createClientView.taxNumber, '74451390E'); async() => {
await page.write(selectors.createClientView.userName, 'CaptainMarvel'); await page.autocompleteSearch(selectors.createClientView.salesPerson, 'salesPerson');
await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es'); await page.autocompleteSearch(selectors.createClientView.businessType, 'florist');
await page.autocompleteSearch(selectors.createClientView.salesPerson, 'salesPerson'); await page.write(selectors.createClientView.taxNumber, '74451390E');
await page.waitToClick(selectors.createClientView.createButton); await page.write(selectors.createClientView.userName, 'CaptainMarvel');
const message = await page.waitForSnackbar(); await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es');
await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Some fields are invalid'); expect(message.text).toContain('Some fields are invalid');
}); }
);
it(`should create a new province`, async() => { it(`should create a new province`, async() => {
await page.waitToClick(selectors.createClientView.addPostCode); await page.waitToClick(selectors.createClientView.addPostCode);
@ -80,9 +83,18 @@ describe('Client create path', () => {
expect(message.text).toContain('Some fields are invalid'); expect(message.text).toContain('Some fields are invalid');
}); });
it(`should attempt to create a new user with all it's data but wrong postal code`, async() => { it(`should attempt to create a new user with all it's data but wrong business type`, async() => {
await page.clearInput(selectors.createClientView.email); await page.clearInput(selectors.createClientView.email);
await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es'); await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es');
await page.clearInput(selectors.createClientView.businessType);
await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('The type of business must be filled in basic data');
});
it(`should attempt to create a new user with all it's data but wrong postal code`, async() => {
await page.autocompleteSearch(selectors.createClientView.businessType, 'florist');
await page.clearInput(selectors.createClientView.postcode); await page.clearInput(selectors.createClientView.postcode);
await page.write(selectors.createClientView.postcode, '479999'); await page.write(selectors.createClientView.postcode, '479999');
await page.waitToClick(selectors.createClientView.createButton); await page.waitToClick(selectors.createClientView.createButton);

View File

@ -112,7 +112,7 @@ describe('Client Edit fiscalData path', () => {
expect(message.text).toContain('Cannot check Equalization Tax in this NIF/CIF'); expect(message.text).toContain('Cannot check Equalization Tax in this NIF/CIF');
}); });
it('should finally edit the fixcal data correctly as VIES isnt checked and fiscal id is valid for EQtax', async() => { it('should edit the fiscal data correctly as VIES isnt checked and fiscal id is valid for EQtax', async() => {
await page.clearInput(selectors.clientFiscalData.fiscalId); await page.clearInput(selectors.clientFiscalData.fiscalId);
await page.write(selectors.clientFiscalData.fiscalId, '94980061C'); await page.write(selectors.clientFiscalData.fiscalId, '94980061C');
await page.waitToClick(selectors.clientFiscalData.saveButton); await page.waitToClick(selectors.clientFiscalData.saveButton);

View File

@ -38,10 +38,13 @@ describe('Client Edit billing data path', () => {
await page.autocompleteSearch(selectors.clientBillingData.newBankEntityCountry, 'España'); await page.autocompleteSearch(selectors.clientBillingData.newBankEntityCountry, 'España');
await page.write(selectors.clientBillingData.newBankEntityCode, '9999'); await page.write(selectors.clientBillingData.newBankEntityCode, '9999');
await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton); await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton);
const message = await page.waitForSnackbar();
await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank'); await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank');
const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value'); const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value');
expect(newcode).toEqual('GTHMCT Gotham City Bank'); expect(newcode).toEqual('GTHMCT Gotham City Bank');
expect(message.text).toContain('Data saved!');
}); });
it(`should confirm the IBAN pay method was sucessfully saved`, async() => { it(`should confirm the IBAN pay method was sucessfully saved`, async() => {

View File

@ -16,13 +16,13 @@ describe('Item summary path', () => {
it('should search for an item', async() => { it('should search for an item', async() => {
await page.doSearch('Ranged weapon'); await page.doSearch('Ranged weapon');
const nResults = await page.countElement(selectors.itemsIndex.searchResult); const resultsCount = await page.countElement(selectors.itemsIndex.searchResult);
await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Ranged weapon'); await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Ranged weapon');
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
const isVisible = await page.isVisible(selectors.itemSummary.basicData); const isVisible = await page.isVisible(selectors.itemSummary.basicData);
expect(nResults).toBe(3); expect(resultsCount).toBe(3);
expect(isVisible).toBeTruthy(); expect(isVisible).toBeTruthy();
}); });
@ -61,12 +61,12 @@ describe('Item summary path', () => {
it('should search for other item', async() => { it('should search for other item', async() => {
await page.doSearch('Melee Reinforced'); await page.doSearch('Melee Reinforced');
const nResults = await page.countElement(selectors.itemsIndex.searchResult); const resultsCount = await page.countElement(selectors.itemsIndex.searchResult);
await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton);
await page.waitForSelector(selectors.itemSummary.basicData, {visible: true}); await page.waitForSelector(selectors.itemSummary.basicData, {visible: true});
expect(nResults).toBe(2); expect(resultsCount).toBe(2);
}); });
it(`should now check the item summary preview shows fields from basic data`, async() => { it(`should now check the item summary preview shows fields from basic data`, async() => {

View File

@ -0,0 +1,71 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Item Create', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'item');
});
afterAll(async() => {
await browser.close();
});
it(`should search for the item Infinity Gauntlet to confirm it isn't created yet`, async() => {
await page.doSearch('Infinity Gauntlet');
const resultsCount = await page.countElement(selectors.itemsIndex.searchResult);
expect(resultsCount).toEqual(0);
});
it('should access to the create item view by clicking the create floating button', async() => {
await page.waitToClick(selectors.itemsIndex.createItemButton);
await page.waitForState('item.create');
});
it('should return to the item index by clickig the cancel button', async() => {
await page.waitToClick(selectors.itemCreateView.cancelButton);
await page.waitForState('item.index');
});
it('should now access to the create item view by clicking the create floating button', async() => {
await page.waitToClick(selectors.itemsIndex.createItemButton);
await page.waitForState('item.create');
});
it('should create the Infinity Gauntlet item', async() => {
await page.write(selectors.itemCreateView.temporalName, 'Infinity Gauntlet');
await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo');
await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares');
await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand');
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm Infinity Gauntlet item was created', async() => {
let result = await page
.waitToGetProperty(selectors.itemBasicData.name, 'value');
expect(result).toEqual('Infinity Gauntlet');
result = await page
.waitToGetProperty(selectors.itemBasicData.type, 'value');
expect(result).toEqual('Crisantemo');
result = await page
.waitToGetProperty(selectors.itemBasicData.intrastat, 'value');
expect(result).toEqual('5080000 Coral y materiales similares');
result = await page
.waitToGetProperty(selectors.itemBasicData.origin, 'value');
expect(result).toEqual('Holand');
});
});

View File

@ -1,105 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Item Create/Clone path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'item');
});
afterAll(async() => {
await browser.close();
});
describe('create', () => {
it(`should search for the item Infinity Gauntlet to confirm it isn't created yet`, async() => {
await page.doSearch('Infinity Gauntlet');
const nResults = await page.countElement(selectors.itemsIndex.searchResult);
expect(nResults).toEqual(0);
});
it('should access to the create item view by clicking the create floating button', async() => {
await page.waitToClick(selectors.itemsIndex.createItemButton);
await page.waitForState('item.create');
});
it('should return to the item index by clickig the cancel button', async() => {
await page.waitToClick(selectors.itemCreateView.cancelButton);
await page.waitForState('item.index');
});
it('should now access to the create item view by clicking the create floating button', async() => {
await page.waitToClick(selectors.itemsIndex.createItemButton);
await page.waitForState('item.create');
});
it('should create the Infinity Gauntlet item', async() => {
await page.write(selectors.itemCreateView.temporalName, 'Infinity Gauntlet');
await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo');
await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares');
await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand');
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm Infinity Gauntlet item was created', async() => {
let result = await page
.waitToGetProperty(selectors.itemBasicData.name, 'value');
expect(result).toEqual('Infinity Gauntlet');
result = await page
.waitToGetProperty(selectors.itemBasicData.type, 'value');
expect(result).toEqual('Crisantemo');
result = await page
.waitToGetProperty(selectors.itemBasicData.intrastat, 'value');
expect(result).toEqual('5080000 Coral y materiales similares');
result = await page
.waitToGetProperty(selectors.itemBasicData.origin, 'value');
expect(result).toEqual('Holand');
});
});
// Issue #2201
// When there is just one result you're redirected automatically to it, so
// it's not possible to use the clone option.
xdescribe('clone', () => {
it('should return to the items index by clicking the return to items button', async() => {
await page.waitToClick(selectors.itemBasicData.goToItemIndexButton);
await page.waitForSelector(selectors.itemsIndex.createItemButton);
await page.waitForState('item.index');
});
it(`should search for the item Infinity Gauntlet`, async() => {
await page.doSearch('Infinity Gauntlet');
const nResults = await page.countElement(selectors.itemsIndex.searchResult);
expect(nResults).toEqual(1);
});
it(`should clone the Infinity Gauntlet`, async() => {
await page.waitForTextInElement(selectors.itemsIndex.searchResult, 'Infinity Gauntlet');
await page.waitToClick(selectors.itemsIndex.searchResultCloneButton);
await page.waitToClick(selectors.itemsIndex.acceptClonationAlertButton);
await page.waitForState('item.tags');
});
it('should search for the item Infinity Gauntlet and find two', async() => {
await page.doSearch('Infinity Gauntlet');
const nResults = await page.countElement(selectors.itemsIndex.searchResult);
expect(nResults).toEqual(2);
});
});
});

View File

@ -16,8 +16,8 @@ describe('Item index path', () => {
}); });
it('should click on the fields to show button to open the list of columns to show', async() => { it('should click on the fields to show button to open the list of columns to show', async() => {
await page.waitToClick(selectors.itemsIndex.fieldsToShowButton); await page.waitToClick(selectors.itemsIndex.shownColumns);
const visible = await page.isVisible(selectors.itemsIndex.fieldsToShowForm); const visible = await page.isVisible(selectors.itemsIndex.shownColumnsList);
expect(visible).toBeTruthy(); expect(visible).toBeTruthy();
}); });
@ -31,7 +31,7 @@ describe('Item index path', () => {
await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); await page.waitToClick(selectors.itemsIndex.intrastadCheckbox);
await page.waitToClick(selectors.itemsIndex.originCheckbox); await page.waitToClick(selectors.itemsIndex.originCheckbox);
await page.waitToClick(selectors.itemsIndex.buyerCheckbox); await page.waitToClick(selectors.itemsIndex.buyerCheckbox);
await page.waitToClick(selectors.itemsIndex.destinyCheckbox); await page.waitToClick(selectors.itemsIndex.densityCheckbox);
await page.waitToClick(selectors.itemsIndex.saveFieldsButton); await page.waitToClick(selectors.itemsIndex.saveFieldsButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -39,6 +39,7 @@ describe('Item index path', () => {
}); });
it('should navigate forth and back to see the images column is still visible', async() => { it('should navigate forth and back to see the images column is still visible', async() => {
await page.closePopup();
await page.waitToClick(selectors.itemsIndex.firstSearchResult); await page.waitToClick(selectors.itemsIndex.firstSearchResult);
await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton);
await page.waitToClick(selectors.globalItems.searchButton); await page.waitToClick(selectors.globalItems.searchButton);
@ -54,7 +55,7 @@ describe('Item index path', () => {
}); });
it('should mark all unchecked boxes to leave the index as it was', async() => { it('should mark all unchecked boxes to leave the index as it was', async() => {
await page.waitToClick(selectors.itemsIndex.fieldsToShowButton); await page.waitToClick(selectors.itemsIndex.shownColumns);
await page.waitToClick(selectors.itemsIndex.idCheckbox); await page.waitToClick(selectors.itemsIndex.idCheckbox);
await page.waitToClick(selectors.itemsIndex.stemsCheckbox); await page.waitToClick(selectors.itemsIndex.stemsCheckbox);
await page.waitToClick(selectors.itemsIndex.sizeCheckbox); await page.waitToClick(selectors.itemsIndex.sizeCheckbox);
@ -63,7 +64,7 @@ describe('Item index path', () => {
await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); await page.waitToClick(selectors.itemsIndex.intrastadCheckbox);
await page.waitToClick(selectors.itemsIndex.originCheckbox); await page.waitToClick(selectors.itemsIndex.originCheckbox);
await page.waitToClick(selectors.itemsIndex.buyerCheckbox); await page.waitToClick(selectors.itemsIndex.buyerCheckbox);
await page.waitToClick(selectors.itemsIndex.destinyCheckbox); await page.waitToClick(selectors.itemsIndex.densityCheckbox);
await page.waitToClick(selectors.itemsIndex.saveFieldsButton); await page.waitToClick(selectors.itemsIndex.saveFieldsButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -71,6 +72,7 @@ describe('Item index path', () => {
}); });
it('should now navigate forth and back to see the ids column is now visible', async() => { it('should now navigate forth and back to see the ids column is now visible', async() => {
await page.closePopup();
await page.waitToClick(selectors.itemsIndex.firstSearchResult); await page.waitToClick(selectors.itemsIndex.firstSearchResult);
await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton);
await page.waitToClick(selectors.globalItems.searchButton); await page.waitToClick(selectors.globalItems.searchButton);

View File

@ -62,7 +62,7 @@ describe('Ticket Create packages path', () => {
expect(result).toEqual('7 : Container medical box 1m'); expect(result).toEqual('7 : Container medical box 1m');
}); });
it(`should confirm the first quantity is just a number and the string part was ignored by the imput number`, async() => { it(`should confirm quantity is just a number and the string part was ignored by the imput number`, async() => {
await page.waitForTextInField(selectors.ticketPackages.firstQuantity, '-99'); await page.waitForTextInField(selectors.ticketPackages.firstQuantity, '-99');
const result = await page.waitToGetProperty(selectors.ticketPackages.firstQuantity, 'value'); const result = await page.waitToGetProperty(selectors.ticketPackages.firstQuantity, 'value');

View File

@ -31,7 +31,7 @@ describe('Entry lastest buys path', () => {
await page.waitForSelector(selectors.entryLatestBuys.fieldAutocomplete, {visible: true}); await page.waitForSelector(selectors.entryLatestBuys.fieldAutocomplete, {visible: true});
}); });
it('should search for the "Description" field and type a new description for the items in each selected buy', async() => { it('should search for the "Description" and type a new one for the items in each selected buy', async() => {
await page.autocompleteSearch(selectors.entryLatestBuys.fieldAutocomplete, 'Description'); await page.autocompleteSearch(selectors.entryLatestBuys.fieldAutocomplete, 'Description');
await page.write(selectors.entryLatestBuys.newValueInput, 'Crafted item'); await page.write(selectors.entryLatestBuys.newValueInput, 'Crafted item');
await page.waitToClick(selectors.entryLatestBuys.acceptEditBuysDialog); await page.waitToClick(selectors.entryLatestBuys.acceptEditBuysDialog);

View File

@ -28,7 +28,7 @@ describe('Entry import, create and edit buys path', () => {
await page.waitForState('entry.card.buy.import'); await page.waitForState('entry.card.buy.import');
}); });
it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { it('should fill the form, import the a JSON file and select items for each import and confirm import', async() => {
let currentDir = process.cwd(); let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; let filePath = `${currentDir}/e2e/assets/07_import_buys.json`;
@ -42,7 +42,8 @@ describe('Entry import, create and edit buys path', () => {
await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846'); await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846');
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); const itemName = 'Melee Reinforced weapon heavy shield 1x0.5m';
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, itemName);
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m');
await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m'); await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m');
@ -88,37 +89,37 @@ describe('Entry import, create and edit buys path', () => {
it('should edit the newest buy', async() => { it('should edit the newest buy', async() => {
await page.clearInput(selectors.entryBuys.secondBuyPackingPrice); await page.clearInput(selectors.entryBuys.secondBuyPackingPrice);
await page.waitForTextInField(selectors.entryBuys.secondBuyPackingPrice, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPackingPrice, '100'); await page.write(selectors.entryBuys.secondBuyPackingPrice, '100');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice); await page.clearInput(selectors.entryBuys.secondBuyGroupingPrice);
await page.waitForTextInField(selectors.entryBuys.secondBuyGroupingPrice, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200'); await page.write(selectors.entryBuys.secondBuyGroupingPrice, '200');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPrice); await page.clearInput(selectors.entryBuys.secondBuyPrice);
await page.waitForTextInField(selectors.entryBuys.secondBuyPrice, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPrice, '300'); await page.write(selectors.entryBuys.secondBuyPrice, '300');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyGrouping); await page.clearInput(selectors.entryBuys.secondBuyGrouping);
await page.waitForTextInField(selectors.entryBuys.secondBuyGrouping, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyGrouping, '400'); await page.write(selectors.entryBuys.secondBuyGrouping, '400');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyPacking); await page.clearInput(selectors.entryBuys.secondBuyPacking);
await page.waitForTextInField(selectors.entryBuys.secondBuyPacking, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyPacking, '500'); await page.write(selectors.entryBuys.secondBuyPacking, '500');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyWeight); await page.clearInput(selectors.entryBuys.secondBuyWeight);
await page.waitForTextInField(selectors.entryBuys.secondBuyWeight, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyWeight, '600'); await page.write(selectors.entryBuys.secondBuyWeight, '600');
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyStickers); await page.clearInput(selectors.entryBuys.secondBuyStickers);
await page.waitForTextInField(selectors.entryBuys.secondBuyStickers, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyStickers, '700'); await page.write(selectors.entryBuys.secondBuyStickers, '700');
await page.waitForSnackbar(); await page.waitForSnackbar();
@ -126,7 +127,7 @@ describe('Entry import, create and edit buys path', () => {
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.clearInput(selectors.entryBuys.secondBuyQuantity); await page.clearInput(selectors.entryBuys.secondBuyQuantity);
await page.waitForTextInField(selectors.entryBuys.secondBuyQuantity, ''); await page.waitForTimeout(250);
await page.write(selectors.entryBuys.secondBuyQuantity, '800'); await page.write(selectors.entryBuys.secondBuyQuantity, '800');
}); });

View File

@ -86,12 +86,12 @@
align-items: center; align-items: center;
&.today { &.today {
color: $color-font-bg; color: $color-font;
& > .day-number { & > .day-number {
border: 2px solid $color-font-link; border: 2px solid $color-font-link;
&:hover { &:hover {
background-color: lighten($color-font-link, 20%); background-color: $color-marginal;
opacity: .8 opacity: .8
} }
} }

View File

@ -50,7 +50,7 @@ vn-chip {
&.alert, &.alert,
&.alert.clickable:hover, &.alert.clickable:hover,
&.alert.clickable:focus { &.alert.clickable:focus {
background-color: $color-alert-medium; background-color: lighten($color-alert, 5%);
} }
&.message, &.message,
&.message.clickable:hover, &.message.clickable:hover,

View File

@ -49,7 +49,7 @@ export default class Contextmenu {
get rowIndex() { get rowIndex() {
if (!this.row) return null; if (!this.row) return null;
const table = this.row.closest('vn-table, .vn-table'); const table = this.row.closest('table, vn-table, .vn-table');
const rows = table.querySelectorAll('[ng-repeat]'); const rows = table.querySelectorAll('[ng-repeat]');
return Array.from(rows).findIndex( return Array.from(rows).findIndex(
@ -67,13 +67,13 @@ export default class Contextmenu {
get cell() { get cell() {
if (!this.target) return null; if (!this.target) return null;
return this.target.closest('vn-td, .vn-td, vn-td-editable'); return this.target.closest('td, vn-td, .vn-td, vn-td-editable');
} }
get cellIndex() { get cellIndex() {
if (!this.row) return null; if (!this.row) return null;
const cells = this.row.querySelectorAll('vn-td, .vn-td, vn-td-editable'); const cells = this.row.querySelectorAll('td, vn-td, .vn-td, vn-td-editable');
return Array.from(cells).findIndex( return Array.from(cells).findIndex(
cellItem => cellItem == this.cell cellItem => cellItem == this.cell
); );
@ -82,8 +82,8 @@ export default class Contextmenu {
get rowHeader() { get rowHeader() {
if (!this.row) return null; if (!this.row) return null;
const table = this.row.closest('vn-table, .vn-table'); const table = this.row.closest('table, vn-table, .vn-table');
const headerCells = table && table.querySelectorAll('vn-thead vn-th'); const headerCells = table && table.querySelectorAll('thead th, vn-thead vn-th');
const headerCell = headerCells && headerCells[this.cellIndex]; const headerCell = headerCells && headerCells[this.cellIndex];
return headerCell; return headerCell;
@ -147,7 +147,7 @@ export default class Contextmenu {
*/ */
isActionAllowed() { isActionAllowed() {
if (!this.target) return false; if (!this.target) return false;
const isTableCell = this.target.closest('vn-td, .vn-td'); const isTableCell = this.target.closest('td, vn-td, .vn-td');
return isTableCell && this.fieldName; return isTableCell && this.fieldName;
} }
@ -172,9 +172,28 @@ export default class Contextmenu {
excludeSelection() { excludeSelection() {
let where = {[this.fieldName]: {neq: this.fieldValue}}; let where = {[this.fieldName]: {neq: this.fieldValue}};
if (this.exprBuilder) { if (this.exprBuilder) {
where = buildFilter(where, (param, value) => where = {[this.fieldName]: this.fieldValue};
this.exprBuilder({param, value}) where = buildFilter(where, (param, value) => {
); const expr = this.exprBuilder({param, value});
const props = Object.keys(expr);
let newExpr = {};
for (let prop of props) {
if (expr[prop].like) {
const operator = expr[prop].like;
newExpr[prop] = {nlike: operator};
} else if (expr[prop].between) {
const operator = expr[prop].between;
newExpr = {
or: [
{[prop]: {lt: operator[0]}},
{[prop]: {gt: operator[1]}},
]
};
} else
newExpr[prop] = {neq: this.fieldValue};
}
return newExpr;
});
} }
this.model.addFilter({where}); this.model.addFilter({where});
@ -204,19 +223,29 @@ export default class Contextmenu {
delete userFilter.where; delete userFilter.where;
} }
function removeProp(instance, findProp, prop) { function removeProp(obj, targetProp, prop) {
if (prop == findProp) if (prop == targetProp)
delete instance[prop]; delete obj[prop];
if (prop === 'and') { if (prop === 'and' || prop === 'or') {
for (let [index, param] of instance[prop].entries()) { const arrayCopy = obj[prop].slice();
for (let param of arrayCopy) {
const [key] = Object.keys(param); const [key] = Object.keys(param);
if (key == findProp) const index = obj[prop].findIndex(param => {
instance[prop].splice(index, 1); return Object.keys(param)[0] == key;
});
if (key == targetProp)
obj[prop].splice(index, 1);
if (param[key] instanceof Array) if (param[key] instanceof Array)
removeProp(param, filterKey, key); removeProp(param, filterKey, key);
if (Object.keys(param).length == 0)
obj[prop].splice(index, 1);
} }
if (obj[prop].length == 0)
delete obj[prop];
} }
} }

View File

@ -71,7 +71,7 @@
color: $color-font; color: $color-font;
&::placeholder { &::placeholder {
color: $color-font-bg; color: $color-font-bg-marginal;
} }
&[type=time], &[type=time],
&[type=date], &[type=date],
@ -116,6 +116,7 @@
&:active, &:active,
&:valid { &:valid {
box-shadow: 0 0 0 40px $color-bg-panel inset; box-shadow: 0 0 0 40px $color-bg-panel inset;
-webkit-text-fill-color: $color-primary-medium
} }
} }
} }
@ -198,7 +199,7 @@
} }
&.standout { &.standout {
border-radius: 1px; border-radius: 1px;
background-color: rgba(255, 255, 255, .1); background-color: rgba(161, 161, 161, 0.1);
padding: 0 12px; padding: 0 12px;
transition-property: background-color, color; transition-property: background-color, color;
transition-duration: 200ms; transition-duration: 200ms;
@ -208,6 +209,17 @@
& > .underline { & > .underline {
display: none; display: none;
} }
& > .infix > .control > input {
&:-internal-autofill-selected {
&,
&:hover,
&:active,
&:valid {
box-shadow: 0 0 0 40px #474747 inset;
-webkit-text-fill-color: $color-font-dark
}
}
}
& > .infix > .control > * { & > .infix > .control > * {
color: $color-font-dark; color: $color-font-dark;
@ -222,11 +234,22 @@
} }
} }
&.focused { &.focused {
background-color: $color-bg-panel; background-color: $color-font-dark;
& > .container { & > .container {
& > .infix > .control > input {
&:-internal-autofill-selected {
&,
&:hover,
&:active,
&:valid {
box-shadow: 0 0 0 40px $color-font-dark inset;
-webkit-text-fill-color: $color-font-bg
}
}
}
& > .infix > .control > * { & > .infix > .control > * {
color: $color-font; color: $color-marginal;
&::placeholder { &::placeholder {
color: $color-font-bg; color: $color-font-bg;
@ -235,7 +258,7 @@
& > .prepend, & > .prepend,
& > .append, & > .append,
& > .icons { & > .icons {
color: $color-font-bg-marginal; color: $color-marginal;
} }
} }
} }

View File

@ -52,3 +52,4 @@ import './wday-picker';
import './datalist'; import './datalist';
import './contextmenu'; import './contextmenu';
import './rating'; import './rating';
import './smart-table';

View File

@ -15,6 +15,7 @@
<div class="icons pre"> <div class="icons pre">
<vn-icon <vn-icon
icon="clear" icon="clear"
ng-show="::$ctrl.clearDisabled != true"
translate-attr="{title: 'Clear'}" translate-attr="{title: 'Clear'}"
ng-click="$ctrl.onClear($event)"> ng-click="$ctrl.onClear($event)">
</vn-icon> </vn-icon>

View File

@ -85,6 +85,7 @@ ngModule.vnComponent('vnInputNumber', {
min: '<?', min: '<?',
max: '<?', max: '<?',
step: '<?', step: '<?',
displayControls: '<?' displayControls: '<?',
clearDisabled: '<?'
} }
}); });

View File

@ -145,9 +145,8 @@ export default class MultiCheck extends FormInput {
toggle() { toggle() {
const data = this.model.data; const data = this.model.data;
if (!data) return; if (!data) return;
data.forEach(el => { for (let el of data)
el[this.checkField] = this.checkAll; el[this.checkField] = this.checkAll;
});
} }
} }
@ -156,8 +155,9 @@ ngModule.vnComponent('vnMultiCheck', {
controller: MultiCheck, controller: MultiCheck,
bindings: { bindings: {
model: '<', model: '<',
checkField: '<?', checkField: '@?',
checkAll: '=?', checkAll: '=?',
checked: '=?',
disabled: '<?' disabled: '<?'
} }
}); });

View File

@ -0,0 +1,102 @@
<div class="vn-pa-md">
<vn-horizontal class="actions">
<div class="actions-left">
<vn-button icon="view_column"
id="shownColumns"
ng-if="$ctrl.options.activeButtons.shownColumns"
ng-click="smartTableColumns.show($event)"
vn-tooltip="Shown columns">
</vn-button>
<div ng-transclude="actions"></div>
</div>
<div class="actions-right">
<div class="totalRows" ng-if="$ctrl.model.data">
{{model.data.length}}
<span translate>results</span>
</div>
<vn-button icon="search"
ng-if="$ctrl.options.activeButtons.search"
ng-click="$ctrl.displaySearch()"
vn-tooltip="Search">
</vn-button>
<div class="button-group"
ng-if="$ctrl.options.activeButtons.crud">
<vn-button icon="add"
ng-click="$ctrl.createRow()"
vn-tooltip="Add new row">
</vn-button>
<vn-button icon="undo"
ng-click="$ctrl.model.undoChanges()"
vn-tooltip="Undo">
</vn-button>
<vn-button icon="delete"
ng-click="deleteConfirmation.show($event)"
ng-show="$ctrl.checkedRows.length > 0"
vn-tooltip="Remove selected rows">
</vn-button>
<vn-button icon="save"
ng-click="$ctrl.saveAll()"
vn-tooltip="Save data">
</vn-button>
</div>
<vn-button icon="refresh"
ng-click="$ctrl.model.refresh()"
vn-tooltip="Refresh">
</vn-button>
</div>
</vn-horizontal>
<div id="table"></div>
<vn-pagination
ng-if="$ctrl.model"
model="$ctrl.model"
class="vn-pt-md">
</vn-pagination>
</div>
<vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteAll()"
question="Are you sure you want to continue?"
message="Remove selected rows">
</vn-confirm>
<vn-crud-model
ng-if="$ctrl.viewConfigId"
vn-id="userViewModel"
url="UserConfigViews"
link="{tableCode: $ctrl.viewConfigId, userFk: $ctrl.currentUserId}"
data="$ctrl.viewConfig"
auto-load="true">
</vn-crud-model>
<vn-popover vn-id="smart-table-columns" message="Fields to show">
<tpl-body>
<div class="smart-table-columns vn-pa-md vn-w-sm">
<vn-horizontal>
<h6 translate style="margin:0">Shown columns</h6>
<vn-icon
vn-none
icon="info"
color-marginal
vn-tooltip="Check the columns you want to see"/>
</vn-horizontal>
<div class="vn-mb-md">
<vn-check label="Tick all"
ng-model="$ctrl.checkAll">
</vn-check>
</div>
<vn-horizontal class="vn-mb-md">
<vn-check ng-repeat="column in $ctrl.columns"
label="{{column.caption}}"
ng-model="$ctrl.viewConfig[0].configuration[column.field]">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-button
label="Save"
ng-click="$ctrl.saveViewConfig()">
</vn-button>
</vn-horizontal>
</div>
</tpl-body>
</vn-popover>

View File

@ -0,0 +1,450 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import {buildFilter} from 'vn-loopback/util/filter';
import angular from 'angular';
import {camelToKebab} from '../../lib/string';
import './style.scss';
import './table.scss';
export default class SmartTable extends Component {
constructor($element, $, $transclude) {
super($element, $);
this.currentUserId = window.localStorage.currentUserWorkerId;
this.$transclude = $transclude;
this.sortCriteria = [];
this.$inputsScope;
this.columns = [];
this.autoSave = false;
this.transclude();
}
$onDestroy() {
const styleElement = document.querySelector('style[id="smart-table"]');
if (this.$.css && styleElement)
styleElement.parentNode.removeChild(styleElement);
}
get options() {
return this._options;
}
set options(options) {
this._options = options;
if (!options) return;
const activeButtons = options.activeButtons;
const missingId = activeButtons && activeButtons.shownColumns && !this.viewConfigId;
if (missingId)
throw new Error('vnSmartTable: View identifier not defined');
}
get model() {
return this._model;
}
set model(value) {
this._model = value;
if (value)
this.$.model = value;
}
get viewConfigId() {
return this._viewConfigId;
}
set viewConfigId(value) {
this._viewConfigId = value;
}
getDefaultViewConfig() {
const url = 'DefaultViewConfigs';
const filter = {where: {tableCode: this.viewConfigId}};
return this.$http.get(url, {filter})
.then(res => {
if (res && res.data.length)
return res.data[0].columns;
});
}
get viewConfig() {
return this._viewConfig;
}
set viewConfig(value) {
this._viewConfig = value;
if (!value) return;
if (!value.length) {
this.getDefaultViewConfig().then(columns => {
const defaultViewConfig = columns ? columns : {};
const userViewModel = this.$.userViewModel;
for (const column of this.columns) {
if (defaultViewConfig[column.field] == undefined)
defaultViewConfig[column.field] = true;
}
userViewModel.insert({
userFk: this.currentUserId,
tableConfig: this.viewConfigId,
configuration: defaultViewConfig
});
}).finally(() => this.applyViewConfig());
} else
this.applyViewConfig();
}
get checkedRows() {
const model = this.model;
if (model && model.data)
return model.data.filter(row => row.$checked);
return null;
}
get checkAll() {
return this._checkAll;
}
set checkAll(value) {
this._checkAll = value;
if (value !== undefined) {
const shownColumns = this.viewConfig[0].configuration;
for (let param in shownColumns)
shownColumns[param] = value;
}
}
transclude() {
const slotTable = this.element.querySelector('#table');
this.$transclude($clone => {
const table = $clone[0];
slotTable.appendChild(table);
this.registerColumns();
this.emptyDataRows();
}, null, 'table');
}
saveViewConfig() {
const userViewModel = this.$.userViewModel;
const [viewConfig] = userViewModel.data;
viewConfig.configuration = Object.assign({}, viewConfig.configuration);
userViewModel.save()
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
.then(() => this.applyViewConfig())
.then(() => this.$.smartTableColumns.hide());
}
applyViewConfig() {
const userViewModel = this.$.userViewModel;
const [viewConfig] = userViewModel.data;
const selectors = [];
for (const column of this.columns) {
if (viewConfig.configuration[column.field] == false) {
const baseSelector = `smart-table[view-config-id="${this.viewConfigId}"] table`;
selectors.push(`${baseSelector} thead > tr > th:nth-child(${column.index + 1})`);
selectors.push(`${baseSelector} tbody > tr > td:nth-child(${column.index + 1})`);
}
}
const styleElement = document.querySelector('style[id="smart-table"]');
if (styleElement)
styleElement.parentNode.removeChild(styleElement);
if (selectors.length) {
const rule = selectors.join(', ') + '{display: none}';
this.$.css = document.createElement('style');
this.$.css.setAttribute('id', 'smart-table');
document.head.appendChild(this.$.css);
this.$.css.appendChild(document.createTextNode(rule));
}
}
registerColumns() {
const header = this.element.querySelector('thead > tr');
if (!header) return;
const columns = header.querySelectorAll('th');
// Click handler
for (const [index, column] of columns.entries()) {
const field = column.getAttribute('field');
if (field) {
const columnElement = angular.element(column);
const caption = columnElement.text().trim();
this.columns.push({field, caption, index});
column.addEventListener('click', () => this.orderHandler(column));
}
}
}
emptyDataRows() {
const header = this.element.querySelector('thead > tr');
const columns = header.querySelectorAll('th');
const tbody = this.element.querySelector('tbody');
if (tbody) {
const noSearch = this.$compile(`
<tr class="empty-rows" ng-if="!model.data">
<td colspan="${columns.length}" translate>Enter a new search</td>
</tr>
`)(this.$);
tbody.appendChild(noSearch[0]);
const noRows = this.$compile(`
<tr class="empty-rows" ng-if="model.data.length == 0">
<td colspan="${columns.length}" translate>No data</td>
</tr>
`)(this.$);
tbody.appendChild(noRows[0]);
}
}
orderHandler(element) {
const field = element.getAttribute('field');
const existingCriteria = this.sortCriteria.find(criteria => {
return criteria.field == field;
});
const isASC = existingCriteria && existingCriteria.sortType == 'ASC';
const isDESC = existingCriteria && existingCriteria.sortType == 'DESC';
if (!existingCriteria) {
this.sortCriteria.push({field: field, sortType: 'ASC'});
element.classList.remove('desc');
element.classList.add('asc');
}
if (isDESC) {
this.sortCriteria.splice(this.sortCriteria.findIndex(criteria => {
return criteria.field == field;
}), 1);
element.classList.remove('desc');
element.classList.remove('asc');
}
if (isASC) {
existingCriteria.sortType = 'DESC';
element.classList.remove('asc');
element.classList.add('desc');
}
this.applySort();
}
displaySearch() {
const header = this.element.querySelector('thead > tr');
if (!header) return;
const tbody = this.element.querySelector('tbody');
const columns = header.querySelectorAll('th');
const hasSearchRow = tbody.querySelector('tr#searchRow');
if (hasSearchRow) {
if (this.$inputsScope)
this.$inputsScope.$destroy();
return hasSearchRow.remove();
}
const searchRow = document.createElement('tr');
searchRow.setAttribute('id', 'searchRow');
this.$inputsScope = this.$.$new();
for (let column of columns) {
const field = column.getAttribute('field');
const cell = document.createElement('td');
if (field) {
let input;
let options;
const columnOptions = this.options && this.options.columns;
if (columnOptions)
options = columnOptions.find(column => column.field == field);
if (options && options.searchable == false) {
searchRow.appendChild(cell);
continue;
}
if (options && options.autocomplete) {
let props = ``;
const autocomplete = options.autocomplete;
for (const prop in autocomplete)
props += `${camelToKebab(prop)}="${autocomplete[prop]}"\n`;
input = this.$compile(`
<vn-autocomplete
class="dense"
name="${field}"
ng-model="searchProps['${field}']"
${props}
on-change="$ctrl.searchByColumn('${field}')"
clear-disabled="true"
/>`)(this.$inputsScope);
} else {
input = this.$compile(`
<vn-textfield
class="dense"
name="${field}"
ng-model="searchProps['${field}']"
ng-keydown="$ctrl.searchWithEvent($event, '${field}')"
clear-disabled="true"
/>`)(this.$inputsScope);
}
cell.appendChild(input[0]);
}
searchRow.appendChild(cell);
}
tbody.prepend(searchRow);
}
searchWithEvent($event, field) {
if ($event.key != 'Enter') return;
this.searchByColumn(field);
}
searchByColumn(field) {
const searchCriteria = this.$inputsScope.searchProps[field];
const emptySearch = 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();
}
addFilter(field, value) {
let where = {[field]: value};
if (this.exprBuilder) {
where = buildFilter(where, (param, value) =>
this.exprBuilder({param, value})
);
}
this.model.addFilter({where});
}
applySort() {
let order = this.sortCriteria.map(criteria => `${criteria.field} ${criteria.sortType}`);
order = order.join(', ');
if (order)
this.model.order = order;
this.model.refresh();
}
filterSanitizer(field) {
const userFilter = this.model.userFilter;
const userParams = this.model.userParams;
const where = userFilter && userFilter.where;
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') {
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};
}
removeFilter() {
this.model.applyFilter(userFilter, userParams);
}
createRow() {
let data = {};
if (this.defaultNewData)
data = this.defaultNewData();
this.model.insert(data);
}
deleteAll() {
for (let row of this.checkedRows)
this.model.removeRow(row);
if (this.autoSave)
this.saveAll();
}
saveAll() {
const model = this.model;
if (!model.isChanged)
return this.vnApp.showError(this.$t('No changes to save'));
return this.model.save()
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
}
SmartTable.$inject = ['$element', '$scope', '$transclude'];
ngModule.vnComponent('smartTable', {
template: require('./index.html'),
controller: SmartTable,
transclude: {
table: '?slotTable',
actions: '?slotActions'
},
bindings: {
model: '<?',
viewConfigId: '@?',
autoSave: '<?',
exprBuilder: '&?',
defaultNewData: '&?',
options: '<?'
}
});

View File

@ -0,0 +1,229 @@
describe('Component smartTable', () => {
let $element;
let controller;
let $httpBackend;
beforeEach(ngModule('vnCore'));
beforeEach(inject(($compile, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$element = $compile(`<smart-table></smart-table>`)($rootScope);
controller = $element.controller('smartTable');
}));
afterEach(() => {
$element.remove();
});
describe('options setter()', () => {
it(`should throw an error if the table doesn't have an identifier`, () => {
const options = {activeButtons: {shownColumns: []}};
expect(() => controller.options = options).toThrowError(/View identifier not defined/);
});
it('should not throw an error if the table does have an identifier', () => {
const options = {activeButtons: {shownColumns: []}};
controller.viewConfigId = 'test';
expect(() => controller.options = options).not.toThrow();
});
});
describe('getDefaultViewConfig()', () => {
it('should perform a query and return the default view columns', done => {
const expectedResponse = [{
columns: {}
}];
$httpBackend.expectGET('DefaultViewConfigs').respond(expectedResponse);
controller.getDefaultViewConfig().then(columns => {
expect(columns).toEqual(expectedResponse[0].columns);
done();
}).catch(done.fail);
$httpBackend.flush();
});
});
describe('viewConfig setter', () => {
it('should just call applyViewConfig() if a viewConfig was provided', () => {
spyOn(controller, 'applyViewConfig');
controller.viewConfig = [{}];
expect(controller.applyViewConfig).toHaveBeenCalled();
});
it('should not get a defaultConfig then insert a new one', () => {
spyOn(controller, 'applyViewConfig');
controller.$.userViewModel = {
insert: jest.fn()
};
const emptyResponse = [{
columns: {}
}];
controller.columns = [
{field: 'test1'},
{field: 'test2'}
];
$httpBackend.expectGET('DefaultViewConfigs').respond(emptyResponse);
controller.viewConfig = [];
$httpBackend.flush();
const expectedObject = {configuration: {'test1': true, 'test2': true}};
expect(controller.$.userViewModel.insert).toHaveBeenCalledWith(expect.objectContaining(expectedObject));
expect(controller.applyViewConfig).toHaveBeenCalled();
});
});
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 = {
where: {
myField: 'myValue'
}
};
expect(controller.model.addFilter).toHaveBeenCalledWith(expectedFilter);
});
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');
const expectedFilter = {
where: {
builtField: 'builtValue'
}
};
expect(controller.model.addFilter).toHaveBeenCalledWith(expectedFilter);
});
});
describe('applySort()', () => {
it('should call the model refresh() without making changes on the model order', () => {
controller.model = {refresh: jest.fn()};
controller.applySort();
expect(controller.model.order).toBeUndefined();
expect(controller.model.refresh).toHaveBeenCalled();
});
it('should call the model.refresh() after setting model order according to the controller sortCriteria', () => {
controller.model = {refresh: jest.fn()};
const orderBy = {field: 'myField', sortType: 'ASC'};
controller.sortCriteria = [orderBy];
controller.applySort();
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
expect(controller.model.refresh).toHaveBeenCalled();
});
});
describe('filterSanitizer()', () => {
it('should remove the where filter after leaving no fields in it', () => {
controller.model = {
userFilter: {
where: {fieldToRemove: 'valueToRemove'}
},
userParams: {}
};
const result = controller.filterSanitizer('fieldToRemove');
const exectedObject = {userFilter: {}, userParams: {}};
expect(result).toEqual(exectedObject);
});
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: {}
};
const result = controller.filterSanitizer('aFieldToRemove');
const exectedObject = {userFilter: {}, userParams: {}};
expect(result).toEqual(exectedObject);
});
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: {}
};
const result = controller.filterSanitizer('aFieldToRemove');
const exectedObject = {userFilter: {where: {or: [{dontKillMe: 'thanks'}]}}, userParams: {}};
expect(result).toEqual(exectedObject);
});
});
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.saveAll();
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
});
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.saveAll().then(() => {
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
done();
}).catch(done.fail);
});
});
});

View File

@ -0,0 +1,9 @@
Remove selected rows: Eliminar líneas seleccionadas
Add new row: Añadir nueva fila
Undo: Deshacer
Save data: Guardar datos
Shown columns: Columnas visibles
Check the columns you want to see: Marca las columnas que quieres ver
Showing: Mostrando
results: resultados
Tick all: Marcar todas

View File

@ -0,0 +1,146 @@
@import "effects";
@import "variables";
smart-table {
th[field] {
overflow: visible;
cursor: pointer;
align-items: center;
}
th[field][number] {
& > :before {
vertical-align: middle;
font-family: 'Material Icons';
content: 'arrow_downward';
color: $color-spacer;
margin-right: 2px;
opacity: 0
}
&.asc > :before, &.desc > :before {
color: $color-font;
opacity: 1;
}
&.asc > :before {
content: 'arrow_upward';
}
&.desc > :before {
content: 'arrow_downward';
}
&:hover > :before {
opacity: 1;
}
}
th[field]:not([number]) {
& > :after {
vertical-align: middle;
font-family: 'Material Icons';
content: 'arrow_downward';
color: $color-spacer;
margin-left: 2px;
opacity: 0
}
&.asc > :after, &.desc > :after {
color: $color-font;
opacity: 1;
}
&.asc > :after {
content: 'arrow_upward';
}
&.desc > :after {
content: 'arrow_downward';
}
&:hover > :after {
opacity: 1;
}
}
tr[vn-anchor] {
@extend %clickable;
}
.totalRows {
color: $color-font-secondary;
}
.actions-left,
.actions-right {
display: flex;
align-items: center;
.button-group {
display: flex;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .3);
& > vn-button {
box-shadow: 0 0 0 0
}
}
}
.actions-left {
justify-content: flex-start;
slot-actions > vn-button,
& > vn-button,
.button-group {
margin-right: 10px
}
slot-actions {
display: flex
}
}
.actions-right {
justify-content: flex-end;
& > vn-button,
.button-group {
margin-left: 10px
}
}
#table {
overflow-x: auto;
margin-top: 15px
}
vn-tbody a[ng-repeat].vn-tr:focus {
background-color: $color-primary-light
}
.new-row {
background-color: $color-success-light
}
.changed-row {
background-color: $color-primary-light
}
}
.smart-table-columns {
h6 {
color: $color-font-secondary
}
& > vn-horizontal {
align-items: flex-start;
flex-wrap: wrap;
}
vn-check {
flex: initial;
width: 33%
}
}

View File

@ -0,0 +1,111 @@
@import "effects";
@import "variables";
smart-table table {
width: 100%;
border-collapse: collapse;
& > thead {
border-bottom: $border;
& > * > th {
font-weight: normal;
}
}
& > tfoot {
border-top: $border;
}
thead, tbody, tfoot {
& > * {
& > th {
color: $color-font-light;
}
& > th,
& > td {
overflow: hidden;
}
& > th,
& > td {
text-align: left;
padding: 5px;
white-space: nowrap;
text-overflow: ellipsis;
&[number] {
text-align: right;
}
&[centered] {
text-align: center;
}
&[shrink] {
width: 1px;
text-align: center;
}
&[shrink-date] {
width: 100px;
max-width: 100px;
}
&[shrink-datetime] {
width: 150px;
max-width: 150px;
}
&[expand] {
max-width: 400px;
min-width: 0;
}
&[actions] {
width: 1px;
& > * {
vertical-align: middle;
}
}
vn-icon.bright, i.bright {
color: #f7931e;
}
}
}
}
tbody > * {
border-bottom: $border-thin;
&:last-child {
border-bottom: none;
}
& > td {
.chip {
padding: 4px;
border-radius: 4px;
&.notice {
background-color: $color-notice-medium;
color: $color-font-bg;
}
&.success {
background-color: $color-success-medium;
color: $color-font-bg;
}
&.warning {
background-color: $color-main-medium;
color: $color-font-bg;
}
&.alert {
background-color: lighten($color-alert, 5%);
color: $color-font-bg;
}
&.message {
background-color: $color-bg-dark;
color: $color-font-dark;
}
}
}
}
.vn-check {
margin: 0;
}
.empty-rows > td {
color: $color-font-secondary;
font-size: 1.375rem;
text-align: center;
}
}

View File

@ -14,7 +14,7 @@ vn-table {
& > vn-thead, & > vn-thead,
& > thead { & > thead {
display: table-header-group; display: table-header-group;
border-bottom: 2px solid $color-spacer; border-bottom: $border;
& > * > th { & > * > th {
font-weight: normal; font-weight: normal;
@ -32,7 +32,7 @@ vn-table {
& > vn-tfoot, & > vn-tfoot,
& > .vn-tfoot, & > .vn-tfoot,
& > tfoot { & > tfoot {
border-top: 2px solid $color-spacer; border-top: $border;
display: table-footer-group display: table-footer-group
} }
& > * > vn-tr, & > * > vn-tr,
@ -119,7 +119,7 @@ vn-table {
vn-tbody > *, vn-tbody > *,
.vn-tbody > *, .vn-tbody > *,
tbody > * { tbody > * {
border-bottom: 1px solid $color-spacer-light; border-bottom: $border-thin;
&:last-child { &:last-child {
border-bottom: none; border-bottom: none;
@ -132,23 +132,26 @@ vn-table {
.chip { .chip {
padding: 4px; padding: 4px;
border-radius: 4px; border-radius: 4px;
color: $color-font-bg;
&.notice { &.notice {
background-color: $color-notice-medium background-color: $color-notice-medium;
color: $color-font-bg;
} }
&.success { &.success {
background-color: $color-success-medium; background-color: $color-success-medium;
color: $color-font-bg;
} }
&.warning { &.warning {
background-color: $color-main-medium; background-color: $color-main-medium;
color: $color-font-bg;
} }
&.alert { &.alert {
background-color: $color-alert-medium; background-color: lighten($color-alert, 5%);
color: $color-font-bg;
} }
&.message { &.message {
background-color: $color-bg-dark;
color: $color-font-dark; color: $color-font-dark;
background-color: $color-bg-dark
} }
} }
vn-icon-menu { vn-icon-menu {
@ -194,8 +197,8 @@ vn-table.scrollable > .vn-table,
vn-thead vn-th, vn-thead vn-th,
thead vn-th, thead vn-th,
thead th { thead th {
border-bottom: 2px solid $color-spacer; border-bottom: $border;
background-color: #FFF; background-color: $color-bg-panel;
position: sticky; position: sticky;
z-index: 9; z-index: 9;
top: 0 top: 0

View File

@ -16,3 +16,4 @@ import './droppable';
import './http-click'; import './http-click';
import './http-submit'; import './http-submit';
import './anchor'; import './anchor';

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -1,5 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import noImage from './no-image.png'; import noImage from './no-image-dark.png';
/** /**
* Sets a default image when there is an error loading * Sets a default image when there is an error loading

View File

@ -23,30 +23,30 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-basketadd:before { .icon-preserved:before {
content: "\e901"; content: "\e963";
} }
.icon-addperson:before { .icon-treatments:before {
content: "\e955"; content: "\e964";
} }
.icon-supplierfalse:before { .icon-funeral:before {
content: "\e965";
}
.icon-handmadeArtificial:before {
content: "\e966";
}
.icon-fixedPrice:before {
content: "\e962"; content: "\e962";
} }
.icon-invoice-in-create:before { .icon-accounts:before {
content: "\e948"; content: "\e95f";
} }
.icon-invoiceOut:before { .icon-clientConsumption:before {
content: "\e960"; content: "\e960";
} }
.icon-invoiceIn:before { .icon-lastBuy:before {
content: "\e961"; content: "\e961";
} }
.icon-supplier:before {
content: "\e936";
}
.icon-latestBuy:before {
content: "\e95f";
}
.icon-zone:before { .icon-zone:before {
content: "\e95d"; content: "\e95d";
} }
@ -86,6 +86,9 @@
.icon-deliveryprices:before { .icon-deliveryprices:before {
content: "\e956"; content: "\e956";
} }
.icon-basketadd:before {
content: "\e955";
}
.icon-catalog:before { .icon-catalog:before {
content: "\e952"; content: "\e952";
} }
@ -128,9 +131,15 @@
.icon-actions:before { .icon-actions:before {
content: "\e900"; content: "\e900";
} }
.icon-addperson:before {
content: "\e901";
}
.icon-albaran:before { .icon-albaran:before {
content: "\e902"; content: "\e902";
} }
.icon-apps:before {
content: "\e948";
}
.icon-artificial:before { .icon-artificial:before {
content: "\e903"; content: "\e903";
} }
@ -239,6 +248,9 @@
.icon-mandatory:before { .icon-mandatory:before {
content: "\e921"; content: "\e921";
} }
.icon-niche:before {
content: "\e922";
}
.icon-no036:before { .icon-no036:before {
content: "\e923"; content: "\e923";
} }
@ -302,6 +314,9 @@
.icon-stowaway:before { .icon-stowaway:before {
content: "\e92c"; content: "\e92c";
} }
.icon-supplier:before {
content: "\e936";
}
.icon-tags:before { .icon-tags:before {
content: "\e937"; content: "\e937";
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -1,6 +1,6 @@
@import "./util"; @import "./util";
$font-size: 12pt; $font-size: 11pt;
$menu-width: 256px; $menu-width: 256px;
$topbar-height: 56px; $topbar-height: 56px;
$mobile-width: 800px; $mobile-width: 800px;
@ -24,7 +24,7 @@ $spacing-xl: 70px;
// Light theme // Light theme
$color-primary: #f7931e; /* $color-primary: #f7931e;
$color-secondary: $color-primary; $color-secondary: $color-primary;
$color-font: #222; $color-font: #222;
@ -72,52 +72,65 @@ $color-success-light: lighten($color-success, 35%);
$color-notice-medium: lighten($color-notice, 20%); $color-notice-medium: lighten($color-notice, 20%);
$color-notice-light: lighten($color-notice, 35%); $color-notice-light: lighten($color-notice, 35%);
$color-alert-medium: lighten($color-alert, 20%); $color-alert-medium: lighten($color-alert, 20%);
$color-alert-light: lighten($color-alert, 35%); $color-alert-light: lighten($color-alert, 35%); */
/**/ /**/
// Dark theme // Dark theme
/* $color-primary: #ec8916;
$color-header: #3d3d3d; $color-secondary: $color-primary;
$color-bg: #222;
$color-bg-dark: #222;
$color-font: #eee; $color-font: #eee;
$color-font-light: #aaa; $color-font-light: #aaa;
$color-font-secondary: #777; $color-font-secondary: #777;
$color-font-dark: white; $color-font-dark: white;
$color-font-link: #66bfff;
$color-font-bg: rgba(0, 0, 0, .8); $color-font-bg: rgba(0, 0, 0, .8);
$color-font-link: #005a9a; $color-font-bg-marginal: rgba(255, 255, 255, .4);
$color-font-bg-dark: rgba(255, 255, 255, .7);
$color-font-bg-dark-marginal: rgba(255, 255, 255, .4);
$color-header: #3d3d3d;
$color-menu-header: #3d3d3d;
$color-bg: #222;
$color-bg-dark: #222;
$color-active: #666; $color-active: #666;
$color-active-font: white; $color-active-font: white;
$color-bg-panel: #3c3b3b; $color-bg-panel: #3c3b3b;
$color-main: #f7931e; $color-main: $color-primary;
$color-marginal: #ccc; $color-marginal: #222;
$color-success: #a3d131; $color-success: #a3d131;
$color-notice: #32b1ce; $color-notice: #32b1ce;
$color-alert: #f42121; $color-alert: #fa3939;
$color-button: $color-secondary;
$color-spacer: rgba(255, 255, 255, .3); $color-spacer: rgba(255, 255, 255, .3);
$color-spacer-light: rgba(255, 255, 255, .12); $color-spacer-light: rgba(255, 255, 255, .12);
$color-input-underline: rgba(255, 255, 255, .12); $color-input-underline: rgba(255, 255, 255, .12);
$color-input-underline-hover: rgba(255, 255, 255, .6); $color-input-underline-hover: rgba(255, 255, 255, .6);
$color-shadow: rgba(0, 0, 0, .2); $color-shadow: rgba(0, 0, 0, .2);
$color-border: rgba(0, 0, 0, .3);
$color-hightlight: rgba(255, 255, 255, .15); $color-hightlight: rgba(255, 255, 255, .15);
$color-hover-cd: rgba(255, 255, 255, .1); $color-hover-cd: rgba(255, 255, 255, .1);
$color-hover-dc: .7; $color-hover-dc: .7;
$color-disabled: .6; $color-disabled: .6;
$color-font-link: lighten($color-main, 10%); $color-primary-medium: lighten($color-primary, 20%);
$color-main-medium: darken($color-main, 20%); $color-primary-light: lighten($color-primary, 35%);
$color-main-light: darken($color-main, 35%); $color-font-link-medium: lighten($color-font-link, 20%);
$color-success-medium: darken($color-success, 20%); $color-font-link-light: lighten($color-font-link, 35%);
$color-success-light: darken($color-success, 35%); $color-main-medium: lighten($color-main, 20%);
$color-notice-medium: darken($color-notice, 20%); $color-main-light: lighten($color-main, 35%);
$color-notice-light: darken($color-notice, 35%); $color-success-medium: lighten($color-success, 20%);
$color-alert-medium: darken($color-alert, 20%); $color-success-light: lighten($color-success, 35%);
$color-alert-light: darken($color-alert, 35%); $color-notice-medium: lighten($color-notice, 20%);
$color-notice-light: lighten($color-notice, 35%);
$color-alert-medium: lighten($color-alert, 20%);
$color-alert-light: lighten($color-alert, 35%);
/**/ /**/
// Border // Border
$border-thin: 1px solid $color-spacer; $border-thin: 1px solid $color-border;
$border-thin-light: 1px solid $color-spacer-light; $border-thin-light: 1px solid $color-spacer-light;
$border: 2px solid $color-border;
$shadow: 0 2px 2px 0 rgba(0, 0, 0, .3); $shadow: 0 2px 2px 0 rgba(0, 0, 0, .3);

View File

@ -23,9 +23,12 @@ vn-layout {
padding-right: 16px; padding-right: 16px;
overflow: hidden; overflow: hidden;
& > .logo > img { & > .logo {
height: 32px; outline: 0;
display: block; & > img {
height: 32px;
display: block;
}
} }
& > .main-title { & > .main-title {
font-size: 1.56rem; font-size: 1.56rem;

View File

@ -1,5 +1,11 @@
<vn-crud-model vn-id="model" url="{{$ctrl.url}}" filter="$ctrl.filter" link="{originFk: $ctrl.originId}" <vn-crud-model
data="$ctrl.logs" limit="20" auto-load="true"> vn-id="model"
url="{{$ctrl.url}}"
filter="$ctrl.filter"
link="{originFk: $ctrl.originId}"
data="$ctrl.logs"
limit="20"
auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model" class="vn-w-xl"> <vn-data-viewer model="model" class="vn-w-xl">
<vn-card> <vn-card>

View File

@ -65,11 +65,10 @@ ui-view > .vn-summary {
text-transform: uppercase; text-transform: uppercase;
font-size: 1.25rem; font-size: 1.25rem;
line-height: 1; line-height: 1;
padding: 7px; padding: 7px 0;
padding-bottom: 4px; /* Bottom line-height fix */ padding-bottom: 5px; /* Bottom line-height fix */
font-weight: lighter; font-weight: lighter;
background-color: $color-main-light; border-bottom: 2px solid $color-main;
border-bottom: 1px solid $color-main;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -78,7 +77,7 @@ ui-view > .vn-summary {
display: block; display: block;
} }
a { a {
color: $color-font; color: $color-font-link;
} }
} }
h4 span:after { h4 span:after {
@ -87,7 +86,7 @@ ui-view > .vn-summary {
position: absolute; position: absolute;
right: 5px; right: 5px;
text-transform: none; text-transform: none;
color: $color-spacer color: $color-font-link;
} }
& > * { & > * {
margin: $spacing-sm; margin: $spacing-sm;

View File

@ -60,7 +60,7 @@ vn-bg-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
.totalBox { .totalBox {
border: 1px solid #CCC; border: $border-thin;
text-align: right; text-align: right;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -128,7 +128,8 @@ async function launchBackTest(done) {
if (err) if (err)
throw err; throw err;
} }
launchBackTest.description = `Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`; launchBackTest.description = `
Runs the backend tests once using a random container, can receive --ci arg to save reports on a xml file`;
// Backend tests // Backend tests

View File

@ -117,5 +117,7 @@
"INACTIVE_PROVIDER": "Inactive provider", "INACTIVE_PROVIDER": "Inactive provider",
"reference duplicated": "reference duplicated", "reference duplicated": "reference duplicated",
"The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option", "The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"This item is not available": "This item is not available" "This item is not available": "This item is not available",
"Deny buy request": "Purchase request for ticket id [{{ticketId}}]({{{url}}}) has been rejected. Reason: {{observation}}",
"The type of business must be filled in basic data": "The type of business must be filled in basic data"
} }

View File

@ -133,6 +133,7 @@
"reserved": "reservado", "reserved": "reservado",
"Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", "Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
"Deny buy request": "Se ha rechazado la petición de compra para el ticket id [{{ticketId}}]({{{url}}}). Motivo: {{observation}}",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
@ -192,6 +193,7 @@
"Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"None": "Ninguno", "None": "Ninguno",
"The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada", "The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada",
"Cannot add more than one '1/2 day vacation'": "No puedes añadir más de un 'Vacaciones 1/2 dia'",
"This document already exists on this ticket": "Este documento ya existe en el ticket", "This document already exists on this ticket": "Este documento ya existe en el ticket",
"Some of the selected tickets are not billable": "Algunos de los tickets seleccionados no son facturables", "Some of the selected tickets are not billable": "Algunos de los tickets seleccionados no son facturables",
"You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes", "You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes",
@ -210,5 +212,7 @@
"Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio", "Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio",
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito", "You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente", "You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente",
"The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'" "The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos",
"You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días"
} }

View File

@ -63,7 +63,7 @@ module.exports = Self => {
}; };
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -20,7 +20,7 @@ module.exports = Self => {
Self.removeFile = async(ctx, id, options) => { Self.removeFile = async(ctx, id, options) => {
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -22,7 +22,7 @@ module.exports = Self => {
let userId = ctx.req.accessToken.userId; let userId = ctx.req.accessToken.userId;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -57,8 +57,14 @@ module.exports = Self => {
} }
}, myOptions); }, myOptions);
const landedPlusWeek = new Date(ticket.landed);
landedPlusWeek.setDate(landedPlusWeek.getDate() + 7);
const isClaimable = landedPlusWeek >= new Date();
if (ticket.isDeleted) if (ticket.isDeleted)
throw new UserError(`You can't create a claim for a removed ticket`); throw new UserError(`You can't create a claim for a removed ticket`);
if (!isClaimable)
throw new UserError(`You can't create a claim from a ticket delivered more than seven days ago`);
const newClaim = await Self.create({ const newClaim = await Self.create({
ticketFk: ticketId, ticketFk: ticketId,

View File

@ -24,7 +24,7 @@ module.exports = Self => {
const resolvedState = 3; const resolvedState = 3;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -1,31 +1,40 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Claim createFromSales()', () => { describe('Claim createFromSales()', () => {
const ticketId = 2; const ticketId = 16;
const newSale = [{ const newSale = [{
id: 3, id: 3,
instance: 0, instance: 0,
quantity: 10 quantity: 10
}]; }];
const ctx = { const activeCtx = {
req: { accessToken: {userId: 1},
accessToken: {userId: 1}, headers: {origin: 'localhost:5000'},
headers: {origin: 'localhost:5000'}, __: () => {}
__: () => {}
}
}; };
const ctx = {
req: activeCtx
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new claim', async() => { it('should create a new claim', async() => {
const tx = await app.models.Claim.beginTransaction({}); const tx = await models.Claim.beginTransaction({});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const claim = await app.models.Claim.createFromSales(ctx, ticketId, newSale, options); const claim = await models.Claim.createFromSales(ctx, ticketId, newSale, options);
expect(claim.ticketFk).toEqual(ticketId); expect(claim.ticketFk).toEqual(ticketId);
let claimBeginning = await app.models.ClaimBeginning.findOne({where: {claimFk: claim.id}}, options); let claimBeginning = await models.ClaimBeginning.findOne({where: {claimFk: claim.id}}, options);
expect(claimBeginning.saleFk).toEqual(newSale[0].id); expect(claimBeginning.saleFk).toEqual(newSale[0].id);
expect(claimBeginning.quantity).toEqual(newSale[0].quantity); expect(claimBeginning.quantity).toEqual(newSale[0].quantity);
@ -37,17 +46,42 @@ describe('Claim createFromSales()', () => {
} }
}); });
it('should not be able to create a claim if exists that sale', async() => { it('should not be able to create a claim for a ticket delivered more than seven days ago', async() => {
const tx = await app.models.Claim.beginTransaction({}); const tx = await models.Claim.beginTransaction({});
let error; let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
await app.models.Claim.createFromSales(ctx, ticketId, newSale, options); const todayMinusEightDays = new Date();
todayMinusEightDays.setDate(todayMinusEightDays.getDate() - 8);
await app.models.Claim.createFromSales(ctx, ticketId, newSale, options); const ticket = await models.Ticket.findById(ticketId, options);
await ticket.updateAttribute('landed', todayMinusEightDays, options);
await models.Claim.createFromSales(ctx, ticketId, newSale, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.toString()).toContain(`You can't create a claim from a ticket delivered more than seven days ago`);
});
it('should not be able to create a claim if exists that sale', async() => {
const tx = await models.Claim.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Claim.createFromSales(ctx, ticketId, newSale, options);
await models.Claim.createFromSales(ctx, ticketId, newSale, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -44,7 +44,7 @@ module.exports = Self => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const args = ctx.args; const args = ctx.args;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -30,7 +30,7 @@ module.exports = Self => {
Self.updateClaimAction = async(ctx, id, options) => { Self.updateClaimAction = async(ctx, id, options) => {
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -54,7 +54,7 @@ module.exports = Self => {
Self.uploadFile = async(ctx, id, options) => { Self.uploadFile = async(ctx, id, options) => {
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -25,7 +25,7 @@ module.exports = function(Self) {
Self.canBeInvoiced = async(id, options) => { Self.canBeInvoiced = async(id, options) => {
const models = Self.app.models; const models = Self.app.models;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -50,7 +50,8 @@ module.exports = function(Self) {
city: data.city, city: data.city,
provinceFk: data.provinceFk, provinceFk: data.provinceFk,
countryFk: data.countryFk, countryFk: data.countryFk,
isEqualizated: data.isEqualizated isEqualizated: data.isEqualizated,
businessTypeFk: data.businessTypeFk
}, myOptions); }, myOptions);
const address = await models.Address.create({ const address = await models.Address.create({

View File

@ -8,7 +8,8 @@ describe('Client Create', () => {
name: 'Wade', name: 'Wade',
socialName: 'Deadpool Marvel', socialName: 'Deadpool Marvel',
street: 'Wall Street', street: 'Wall Street',
city: 'New York' city: 'New York',
businessTypeFk: 'florist'
}; };
it(`should not find Deadpool as he's not created yet`, async() => { it(`should not find Deadpool as he's not created yet`, async() => {
@ -45,6 +46,7 @@ describe('Client Create', () => {
expect(client.email).toEqual(newAccount.email); expect(client.email).toEqual(newAccount.email);
expect(client.fi).toEqual(newAccount.fi); expect(client.fi).toEqual(newAccount.fi);
expect(client.socialName).toEqual(newAccount.socialName); expect(client.socialName).toEqual(newAccount.socialName);
expect(client.businessTypeFk).toEqual(newAccount.businessTypeFk);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -55,6 +55,15 @@ module.exports = Self => {
with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/ with: /^[\w|.|-]+@[\w|-]+(\.[\w|-]+)*(,[\w|.|-]+@[\w|-]+(\.[\w|-]+)*)*$/
}); });
Self.validate('businessTypeFk', hasBusinessType, {
message: `The type of business must be filled in basic data`
});
function hasBusinessType(err) {
if (!this.businessTypeFk)
err();
}
Self.validatesLengthOf('postcode', { Self.validatesLengthOf('postcode', {
allowNull: true, allowNull: true,
allowBlank: true, allowBlank: true,
@ -129,7 +138,8 @@ module.exports = Self => {
function hasIban(err, done) { function hasIban(err, done) {
Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => { Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => {
if (instance && instance.ibanRequired && !this.iban) const isMissingIban = instance && instance.isIbanRequiredForClients && !this.iban;
if (isMissingIban)
err(); err();
done(); done();
}); });

View File

@ -130,6 +130,12 @@
"mysql": { "mysql": {
"columnName": "transactionTypeSageFk" "columnName": "transactionTypeSageFk"
} }
},
"businessTypeFk": {
"type": "string",
"mysql": {
"columnName": "businessTypeFk"
}
} }
}, },
"relations": { "relations": {

View File

@ -25,7 +25,10 @@
"outstandingDebt": { "outstandingDebt": {
"type": "Number" "type": "Number"
}, },
"ibanRequired": { "isIbanRequiredForClients": {
"type": "boolean"
},
"isIbanRequiredForSuppliers": {
"type": "boolean" "type": "boolean"
} }
} }

View File

@ -19,7 +19,7 @@
vn-acl="salesAssistant" vn-acl="salesAssistant"
ng-model="$ctrl.client.payMethodFk" ng-model="$ctrl.client.payMethodFk"
data="paymethods" data="paymethods"
fields="['ibanRequired']" fields="['isIbanRequiredForClients']"
initial-data="$ctrl.client.payMethod"> initial-data="$ctrl.client.payMethod">
</vn-autocomplete> </vn-autocomplete>
<vn-input-number <vn-input-number

View File

@ -5,7 +5,7 @@
limit="20" limit="20"
user-params="::$ctrl.filterParams" user-params="::$ctrl.filterParams"
data="sales" data="sales"
order="itemTypeFk, itemName, itemSize"> order="itemTypeFk, itemName, itemSize, description">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar
@ -42,7 +42,7 @@
<vn-th field="itemFk" number>Item</vn-th> <vn-th field="itemFk" number>Item</vn-th>
<vn-th field="ticketFk" number>Ticket</vn-th> <vn-th field="ticketFk" number>Ticket</vn-th>
<vn-th field="shipped" expand>Fecha</vn-th> <vn-th field="shipped" expand>Fecha</vn-th>
<vn-th expand>Description</vn-th> <vn-th field="description" expand>Description</vn-th>
<vn-th field="quantity" number>Quantity</vn-th> <vn-th field="quantity" number>Quantity</vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>

View File

@ -26,18 +26,28 @@
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-autocomplete
vn-two vn-id="businessTypeFk"
label="Business name" ng-model="$ctrl.client.businessTypeFk"
ng-model="$ctrl.client.socialName" url="BusinessTypes"
show-field="description"
value-field="code"
label="Business type"
rule> rule>
</vn-textfield> </vn-autocomplete>
<vn-textfield <vn-textfield
label="Tax number" label="Tax number"
ng-model="$ctrl.client.fi" ng-model="$ctrl.client.fi"
rule> rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Business name"
ng-model="$ctrl.client.socialName"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield
vn-two vn-two

View File

@ -7,4 +7,5 @@ Create and edit: Crear y editar
You can save multiple emails: >- You can save multiple emails: >-
Puede guardar varios correos electrónicos encadenándolos mediante comas Puede guardar varios correos electrónicos encadenándolos mediante comas
sin espacios, ejemplo: user@dominio.com, user2@dominio.com siendo el primer sin espacios, ejemplo: user@dominio.com, user2@dominio.com siendo el primer
correo electrónico el principal correo electrónico el principal
The type of business must be filled in basic data: El tipo de negocio debe estar rellenado en datos básicos

View File

@ -17,8 +17,8 @@
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
<vn-label-value <vn-label-value
label="Phone" label="Pay method"
value="{{$ctrl.client.phone | phone}}"> value="{{$ctrl.client.payMethod.name}}">
</vn-label-value> </vn-label-value>
<vn-label-value <vn-label-value
label="Credit" label="Credit"

View File

@ -3,4 +3,5 @@ View consumer report: Ver informe de consumo
From date: Fecha desde From date: Fecha desde
To date: Fecha hasta To date: Fecha hasta
Go to user: Ir al usuario Go to user: Ir al usuario
Client invoices list: Listado de facturas del cliente Client invoices list: Listado de facturas del cliente
Pay method: Forma de pago

View File

@ -68,7 +68,7 @@ module.exports = Self => {
Self.addBuy = async(ctx, options) => { Self.addBuy = async(ctx, options) => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -21,7 +21,7 @@ module.exports = Self => {
Self.deleteBuys = async(ctx, options) => { Self.deleteBuys = async(ctx, options) => {
const models = Self.app.models; const models = Self.app.models;
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -32,7 +32,7 @@ module.exports = Self => {
Self.editLatestBuys = async(field, newValue, lines, options) => { Self.editLatestBuys = async(field, newValue, lines, options) => {
let tx; let tx;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -108,7 +108,7 @@ module.exports = Self => {
}); });
Self.filter = async(ctx, filter, options) => { Self.filter = async(ctx, filter, options) => {
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -29,7 +29,7 @@ module.exports = Self => {
Self.getBuys = async(id, filter, options) => { Self.getBuys = async(id, filter, options) => {
const models = Self.app.models; const models = Self.app.models;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -21,7 +21,7 @@ module.exports = Self => {
Self.getEntry = async(id, options) => { Self.getEntry = async(id, options) => {
const models = Self.app.models; const models = Self.app.models;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -26,7 +26,7 @@ module.exports = Self => {
Self.importBuysPreview = async(id, buys, options) => { Self.importBuysPreview = async(id, buys, options) => {
const models = Self.app.models; const models = Self.app.models;
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);

View File

@ -39,6 +39,11 @@ module.exports = Self => {
type: 'integer', type: 'integer',
description: 'The buyer of the item', description: 'The buyer of the item',
}, },
{
arg: 'supplierFk',
type: 'integer',
description: 'The supplier of the item',
},
{ {
arg: 'active', arg: 'active',
type: 'boolean', type: 'boolean',
@ -49,6 +54,11 @@ module.exports = Self => {
type: 'boolean', type: 'boolean',
description: 'Whether the item is or not visible', description: 'Whether the item is or not visible',
}, },
{
arg: 'floramondo',
type: 'boolean',
description: 'Whether the item is or not floramondo',
},
{ {
arg: 'typeFk', arg: 'typeFk',
type: 'integer', type: 'integer',
@ -63,6 +73,16 @@ module.exports = Self => {
arg: 'packingOut', arg: 'packingOut',
type: 'integer', type: 'integer',
description: 'the packingOut', description: 'the packingOut',
},
{
arg: 'from',
type: 'date',
description: `The from date filter`
},
{
arg: 'to',
type: 'date',
description: `The to date filter`
} }
], ],
returns: { returns: {
@ -76,7 +96,7 @@ module.exports = Self => {
}); });
Self.latestBuysFilter = async(ctx, filter, options) => { Self.latestBuysFilter = async(ctx, filter, options) => {
let myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
@ -99,10 +119,18 @@ module.exports = Self => {
return {'ic.id': value}; return {'ic.id': value};
case 'salesPersonFk': case 'salesPersonFk':
return {'it.workerFk': value}; return {'it.workerFk': value};
case 'supplierFk':
return {'s.id': value};
case 'code': case 'code':
return {'it.code': value}; return {'it.code': value};
case 'active': case 'active':
return {'i.isActive': value}; return {'i.isActive': value};
case 'floramondo':
return {'i.isFloramondo': value};
case 'from':
return {'lb.landing': {gte: value}};
case 'to':
return {'lb.landing': {lte: value}};
case 'visible': case 'visible':
if (value) if (value)
return {'v.visible': {gt: 0}}; return {'v.visible': {gt: 0}};
@ -162,7 +190,8 @@ module.exports = Self => {
b.price3, b.price3,
b.ektFk, b.ektFk,
b.weight, b.weight,
b.packageFk b.packageFk,
lb.landing
FROM cache.last_buy lb FROM cache.last_buy lb
LEFT JOIN cache.visible v ON v.item_id = lb.item_id LEFT JOIN cache.visible v ON v.item_id = lb.item_id
AND v.calc_id = @calc_id AND v.calc_id = @calc_id
@ -172,7 +201,9 @@ module.exports = Self => {
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN itemType t ON t.id = i.typeFk LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN origin ori ON ori.id = i.originFk` LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN entry e ON e.id = b.entryFk AND e.created >= DATE_SUB(CURDATE(),INTERVAL 1 YEAR)
LEFT JOIN supplier s ON s.id = e.supplierFk`
); );
if (ctx.args.tags) { if (ctx.args.tags) {

View File

@ -113,6 +113,30 @@ describe('Buy latests buys filter()', () => {
expect(results.length).toBe(0); expect(results.length).toBe(0);
}); });
it('should return results matching "floramondo"', async() => {
let ctx = {
args: {
floramondo: true
}
};
let results = await app.models.Buy.latestBuysFilter(ctx);
expect(results.length).toBe(1);
});
it('should return results matching "not floramondo"', async() => {
let ctx = {
args: {
floramondo: false
}
};
let results = await app.models.Buy.latestBuysFilter(ctx);
expect(results.length).toBe(5);
});
it('should return results matching "salesPersonFk"', async() => { it('should return results matching "salesPersonFk"', async() => {
let ctx = { let ctx = {
args: { args: {
@ -136,5 +160,36 @@ describe('Buy latests buys filter()', () => {
expect(results.length).toBe(1); expect(results.length).toBe(1);
}); });
it('should return results matching "supplierFk"', async() => {
let ctx = {
args: {
supplierFk: 1
}
};
let results = await app.models.Buy.latestBuysFilter(ctx);
expect(results.length).toBe(2);
});
it('should return results matching "from" and "to"', async() => {
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date();
to.setHours(23, 59, 59, 999);
let ctx = {
args: {
from: from,
to: to
}
};
let results = await app.models.Buy.latestBuysFilter(ctx);
expect(results.length).toBe(2);
});
}); });

View File

@ -63,8 +63,21 @@ describe('Entry', () => {
} }
]}`; ]}`;
const expectedBuys = [ const expectedBuys = [
{'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, {
{'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} 'buyingValue': 5.77,
'description': 'Bow',
'grouping': 1,
'packing': 1,
'size': 1,
'volume': 1200},
{
'buyingValue': 2.16,
'description': 'Arrow',
'grouping': 1,
'packing': 1,
'size': 25,
'volume': 1125}
]; ];
controller.fillData(rawData); controller.fillData(rawData);
controller.$.$apply(); controller.$.$apply();
@ -81,8 +94,21 @@ describe('Entry', () => {
describe('fetchBuys()', () => { describe('fetchBuys()', () => {
it(`should perform a query to fetch the buys data`, () => { it(`should perform a query to fetch the buys data`, () => {
const buys = [ const buys = [
{'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, {
{'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} 'buyingValue': 5.77,
'description': 'Bow',
'grouping': 1,
'packing': 1,
'size': 1,
'volume': 1200},
{
'buyingValue': 2.16,
'description': 'Arrow',
'grouping': 1,
'packing': 1,
'size': 25,
'volume': 1125}
]; ];
const serializedParams = $httpParamSerializer({buys}); const serializedParams = $httpParamSerializer({buys});
@ -105,17 +131,31 @@ describe('Entry', () => {
observation: '123456', observation: '123456',
ref: '1, 2', ref: '1, 2',
buys: [ buys: [
{'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, {
{'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} 'buyingValue': 5.77,
'description': 'Bow',
'grouping': 1,
'packing': 1,
'size': 1,
'volume': 1200},
{
'buyingValue': 2.16,
'description': 'Arrow',
'grouping': 1,
'packing': 1,
'size': 25,
'volume': 1125}
] ]
}; };
controller.onSubmit(); controller.onSubmit();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`Some of the imported buys doesn't have an item`); const message = `Some of the imported buys doesn't have an item`;
expect(controller.vnApp.showError).toHaveBeenCalledWith(message);
}); });
it(`should perform a query to update columns`, () => { it(`should now perform a query to update columns`, () => {
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.go = jest.fn(); controller.$state.go = jest.fn();
@ -123,8 +163,22 @@ describe('Entry', () => {
observation: '123456', observation: '123456',
ref: '1, 2', ref: '1, 2',
buys: [ buys: [
{'itemFk': 10, 'buyingValue': 5.77, 'description': 'Bow', 'grouping': 1, 'packing': 1, 'size': 1, 'volume': 1200}, {
{'itemFk': 11, 'buyingValue': 2.16, 'description': 'Arrow', 'grouping': 1, 'packing': 1, 'size': 25, 'volume': 1125} 'itemFk': 10,
'buyingValue': 5.77,
'description': 'Bow',
'grouping': 1,
'packing': 1,
'size': 1,
'volume': 1200},
{
'itemFk': 11,
'buyingValue': 2.16,
'description': 'Arrow',
'grouping': 1,
'packing': 1,
'size': 25,
'volume': 1125}
] ]
}; };
const params = controller.import; const params = controller.import;

View File

@ -188,22 +188,19 @@
<tr><td></td></tr> <tr><td></td></tr>
</tbody> </tbody>
</table> </table>
<div>
<vn-icon-button
vn-one
vn-tooltip="Add buy"
vn-bind="+"
icon="add_circle"
ng-click="model.insert({})">
</vn-icon-button>
</div>
</div> </div>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>
<div fixed-bottom-right> <div fixed-bottom-right>
<vn-vertical style="align-items: center;"> <vn-vertical style="align-items: center;">
<a ui-sref="entry.card.buy.import" <vn-button class="round md vn-mb-sm"
ng-click="model.insert({})"
icon="add"
vn-tooltip="Add buy"
tooltip-position="left"
vn-bind="+"> vn-bind="+">
</vn-button>
<a ui-sref="entry.card.buy.import" >
<vn-button class="round md vn-mb-sm" <vn-button class="round md vn-mb-sm"
icon="publish" icon="publish"
vn-tooltip="Import buys" vn-tooltip="Import buys"

View File

@ -15,16 +15,16 @@ vn-entry-buy-index vn-card {
tbody tr:nth-child(1), tbody tr:nth-child(1),
tbody tr:nth-child(2) { tbody tr:nth-child(2) {
border-left: 1px solid $color-marginal; border-left: 1px solid $color-spacer;
border-right: 1px solid $color-marginal; border-right: 1px solid $color-spacer;
} }
tbody tr:nth-child(2) { tbody tr:nth-child(2) {
border-bottom: 1px solid $color-marginal; border-bottom: 1px solid $color-spacer;
} }
tbody{ tbody{
border-bottom: 1px solid $color-marginal; border-bottom: 1px solid $color-spacer;
} }
tbody:last-child { tbody:last-child {

View File

@ -46,6 +46,29 @@
where="{role: {inq: ['logistic', 'buyer']}}" where="{role: {inq: ['logistic', 'buyer']}}"
label="Buyer"> label="Buyer">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete
vn-one
label="Supplier"
ng-model="filter.supplierFk"
url="Suppliers"
fields="['name','nickname']"
search-function="{or: [{nickname: {like: '%'+ $search +'%'}}, {name: {like: '%'+ $search +'%'}}]}"
show-field="name"
value-field="id">
<tpl-item>{{name}}: {{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
</vn-date-picker>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check
@ -58,6 +81,11 @@
ng-model="filter.visible" ng-model="filter.visible"
triple-state="true"> triple-state="true">
</vn-check> </vn-check>
<vn-check
label="Is floramondo"
ng-model="filter.floramondo"
triple-state="true">
</vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal class="vn-pt-sm"> <vn-horizontal class="vn-pt-sm">
<vn-one class="text-subtitle1" translate> <vn-one class="text-subtitle1" translate>
@ -81,18 +109,21 @@
on-change="itemTag.value = null"> on-change="itemTag.value = null">
</vn-autocomplete> </vn-autocomplete>
<vn-textfield <vn-textfield
ng-show="tag.selection.isFree !== false" ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
vn-id="text" vn-id="text"
label="Value" label="Value"
ng-model="itemTag.value"> ng-model="itemTag.value">
</vn-textfield> </vn-textfield>
<vn-autocomplete <vn-autocomplete
vn-one
ng-show="tag.selection.isFree === false" ng-show="tag.selection.isFree === false"
url="{{$ctrl.getSourceTable(tag.selection)}}" url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
search-function="{value: $search}"
label="Value" label="Value"
ng-model="itemTag.value" ng-model="itemTag.value"
show-field="name" show-field="value"
value-field="name"> value-field="value"
rule>
</vn-autocomplete> </vn-autocomplete>
<vn-icon-button <vn-icon-button
vn-none vn-none

View File

@ -55,21 +55,9 @@ class Controller extends SearchPanel {
this.$.filter = value; this.$.filter = value;
} }
getSourceTable(selection) {
if (!selection || selection.isFree === true)
return null;
if (selection.sourceTable) {
return ''
+ selection.sourceTable.charAt(0).toUpperCase()
+ selection.sourceTable.substring(1) + 's';
} else if (selection.sourceTable == null)
return `ItemTags/filterItemTags/${selection.id}`;
}
removeField(index, field) { removeField(index, field) {
this.fieldFilters.splice(index, 1); this.fieldFilters.splice(index, 1);
this.$.filter[field] = undefined; delete this.$.filter[field];
} }
} }

View File

@ -12,33 +12,48 @@ describe('Entry', () => {
controller = $componentController('vnLatestBuysSearchPanel', {$element}); controller = $componentController('vnLatestBuysSearchPanel', {$element});
})); }));
describe('getSourceTable()', () => { describe('filter() setter', () => {
it(`should return null if there's no selection`, () => { it(`should set the tags property to the scope filter with an empty array`, () => {
let selection = null; const expectedFilter = {
let result = controller.getSourceTable(selection); tags: [{}]
};
controller.filter = null;
expect(result).toBeNull(); expect(controller.filter).toEqual(expectedFilter);
}); });
it(`should return null if there's a selection but its isFree property is truthy`, () => { it(`should set the tags property to the scope filter with an array of tags`, () => {
let selection = {isFree: true}; const expectedFilter = {
let result = controller.getSourceTable(selection); description: 'My item',
tags: [{}]
};
const expectedFieldFilter = [{
info: {
label: 'description',
name: 'description',
type: null
},
name: 'description',
value: 'My item'
}];
controller.filter = {
description: 'My item'
};
expect(result).toBeNull(); expect(controller.filter).toEqual(expectedFilter);
expect(controller.fieldFilters).toEqual(expectedFieldFilter);
}); });
});
it(`should return the formated sourceTable concatenated to a path`, () => { describe('removeField()', () => {
let selection = {sourceTable: 'hello guy'}; it(`should remove the description property from the fieldFilters and from the scope filter`, () => {
let result = controller.getSourceTable(selection); const expectedFilter = {tags: [{}]};
controller.filter = {description: 'My item'};
expect(result).toEqual('Hello guys'); controller.removeField(0, 'description');
});
it(`should return a path if there's no sourceTable and the selection has an id`, () => { expect(controller.filter).toEqual(expectedFilter);
let selection = {id: 99}; expect(controller.fieldFilters).toEqual([]);
let result = controller.getSourceTable(selection);
expect(result).toEqual(`ItemTags/filterItemTags/${selection.id}`);
}); });
}); });
}); });

View File

@ -16,146 +16,207 @@
auto-state="false"> auto-state="false">
</vn-searchbar> </vn-searchbar>
</vn-portal> </vn-portal>
<vn-data-viewer <vn-card>
model="model" <smart-table
class="vn-mb-xl vn-w-xl">
<vn-card>
<vn-table
model="model" model="model"
show-fields="$ctrl.showFields" view-config-id="latestBuys"
vn-smart-table="latestBuys"> options="$ctrl.smartTableOptions"
<vn-thead> expr-builder="$ctrl.exprBuilder(param, value)">
<vn-tr> <slot-table>
<vn-th shrink> <table>
<vn-multi-check <thead>
model="model"> <tr>
</vn-multi-check> <th shrink>
</vn-th> <vn-multi-check
<vn-th field="Image">Picture</vn-th> checked="$ctrl.checkAll"
<vn-th smart-table-ignore field="id">Id</vn-th> model="model"
<vn-th field="packing">Packing</vn-th> check-field="$checked">
<vn-th field="grouping">Grouping</vn-th> </vn-multi-check>
<vn-th field="quantity">Quantity</vn-th> </th>
<vn-th field="description" style="text-align: center">Description</vn-th> <th translate>Picture</th>
<vn-th field="size">Size</vn-th> <th field="id">
<vn-th field="name" style="text-align: center">Tags</vn-th> <span translate>Identifier</span>
<vn-th field="code">Type</vn-th> </th>
<vn-th field="intrastat">Intrastat</vn-th> <th field="packing" number>
<vn-th field="origin">Origin</vn-th> <span translate>Packing</span>
<vn-th field="density">Density</vn-th> </th>
<vn-th field="isActive">Active</vn-th> <th field="grouping" number>
<vn-th field="family">Family</vn-th> <span translate>Grouping</span>
<vn-th field="entryFk">Entry</vn-th> </th>
<vn-th field="buyingValue">Buying value</vn-th> <th field="quantity" number>
<vn-th field="freightValue">Freight value</vn-th> <span translate>Quantity</span>
<vn-th field="comissionValue" expand>Commission value</vn-th> </th>
<vn-th field="packageValue" expand>Package value</vn-th> <th field="description">
<vn-th field="isIgnored">Is ignored</vn-th> <span translate>Description</span>
<vn-th expand field="price2">Grouping price</vn-th> </th>
<vn-th expand field="price3">Packing price</vn-th> <th field="size">
<vn-th field="minPrice">Min price</vn-th> <span translate>Size</span>
<vn-th field="ektFk">Ekt</vn-th> </th>
<vn-th field="weight">Weight</vn-th> <th field="name">
<vn-th field="packageFk" expand>PackageName</vn-th> <span translate>Tags</span>
<vn-th field="packingOut" expand>PackingOut</vn-th> </th>
</vn-tr> <th field="code">
</vn-thead> <span translate>Type</span>
<vn-tbody> </th>
<a ng-repeat="buy in $ctrl.buys" <th field="intrastat">
class="clickable vn-tr search-result" <span translate>Intrastat</span>
ui-sref="entry.card.buy.index({id: {{::buy.entryFk}}})"> </th>
<vn-td shrink> <th field="origin">
<vn-check <span translate>Origin</span>
ng-model="buy.checked" </th>
vn-click-stop> <th field="density">
</vn-check> <span translate>Density</span>
</vn-td> </th>
<vn-td shrink > <th field="isActive">
<img <span translate>Active</span>
ng-src="{{::$root.imagePath('catalog', '50x50', buy.itemFk)}}" </th>
zoom-image="{{::$root.imagePath('catalog', '1600x900', buy.itemFk)}}" <th field="family">
vn-click-stop <span translate>Family</span>
on-error-src/> </th>
</vn-td> <th field="entryFk">
<vn-td shrink> <span translate>Entry</span>
<span </th>
vn-click-stop="itemDescriptor.show($event, buy.itemFk)" <th field="buyingValue" number>
class="link"> <span translate>Buying value</span>
{{::buy.itemFk | zeroFill:6}} </th>
</span> <th field="freightValue" number>
</vn-td> <span translate>Freight value</span>
<vn-td number> </th>
<vn-chip class="transparent" translate-attr="buy.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': buy.groupingMode == 2}"> <th field="comissionValue" number>
<span translate>{{::buy.packing | dashIfEmpty}}</span> <span translate>Commission value</span>
</vn-chip> </th>
</vn-td> <th field="packageValue" number>
<vn-td number> <span translate>Package value</span>
<vn-chip class="transparent" translate-attr="buy.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': buy.groupingMode == 1}"> </th>
<span translate>{{::buy.grouping | dashIfEmpty}}</span> <th field="isIgnored">
</vn-chip> <span translate>Is ignored</span>
</vn-td> </th>
<vn-td number>{{::buy.quantity}}</vn-td> <th field="price2" number>
<vn-td vn-two title="{{::buy.description}}"> <span translate>Grouping</span>
{{::buy.description | dashIfEmpty}} </th>
</vn-td> <th field="price3" number>
<vn-td number>{{::buy.size}}</vn-td> <span translate>Packing</span>
<vn-td vn-fetched-tags> </th>
<div> <th field="minPrice" number>
<vn-one title="{{::buy.name}}">{{::buy.name}}</vn-one> <span translate>Min</span>
<vn-one ng-if="::buy.subName"> </th>
<h3 title="{{::buy.subName}}">{{::buy.subName}}</h3> <th field="ektFk">
</vn-one> <span translate>Ekt</span>
</div> </th>
<vn-fetched-tags <th field="weight">
max-length="6" <span translate>Weight</span>
item="::buy" </th>
tabindex="-1"> <th field="packageFk">
</vn-fetched-tags> <span translate>Package</span>
</vn-td> </th>
<vn-td shrink title="{{::buy.type}}"> <th field="packingOut">
{{::buy.code}} <span translate>Package out</span>
</vn-td> </th>
<vn-td shrink title="{{::item.intrastat}}"> <th field="landing">
{{::buy.intrastat}} <span translate>Landing</span>
</vn-td> </th>
<vn-td shrink>{{::buy.origin}}</vn-td> </tr>
<vn-td shrink>{{::buy.density}}</vn-td> </thead>
<vn-td shrink> <tbody>
<vn-check <tr ng-repeat="buy in $ctrl.buys"
disabled="true" vn-anchor="::{
ng-model="::buy.isActive"> state: 'entry.card.buy.index',
</vn-check> params: {id: {{::buy.entryFk}}}
</vn-td> }">
<vn-td shrink>{{::buy.family}}</vn-td> <td>
<vn-td shrink> <vn-check
<span ng-model="buy.$checked"
vn-click-stop="entryDescriptor.show($event, buy.entryFk)" vn-click-stop>
class="link"> </vn-check>
{{::buy.entryFk}} </td>
</span> <td >
</vn-td> <img
<vn-td number>{{::buy.buyingValue | currency: 'EUR':2}}</vn-td> ng-src="{{::$root.imagePath('catalog', '50x50', buy.itemFk)}}"
<vn-td number>{{::buy.freightValue | currency: 'EUR':2}}</vn-td> zoom-image="{{::$root.imagePath('catalog', '1600x900', buy.itemFk)}}"
<vn-td number>{{::buy.comissionValue | currency: 'EUR':2}}</vn-td> vn-click-stop
<vn-td number>{{::buy.packageValue | currency: 'EUR':2}}</vn-td> on-error-src/>
<vn-td shrink> </td>
<vn-check <td>
disabled="true" <span
ng-model="::buy.isIgnored"> vn-click-stop="itemDescriptor.show($event, buy.itemFk)"
</vn-check> class="link">
</vn-td> {{::buy.itemFk}}
<vn-td number>{{::buy.price2 | currency: 'EUR':2}}</vn-td> </span>
<vn-td number>{{::buy.price3 | currency: 'EUR':2}}</vn-td> </td>
<vn-td number>{{::buy.minPrice | currency: 'EUR':2}}</vn-td> <td number>
<vn-td number>{{::buy.ektFk | dashIfEmpty}}</vn-td> <vn-chip class="transparent" translate-attr="buy.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': buy.groupingMode == 2}">
<vn-td number>{{::buy.weight}}</vn-td> <span translate>{{::buy.packing | dashIfEmpty}}</span>
<vn-td number>{{::buy.packageFk}}</vn-td> </vn-chip>
<vn-td number>{{::buy.packingOut}}</vn-td> </td>
</a> <td number>
</vn-tbody> <vn-chip class="transparent" translate-attr="buy.groupingMode == 1 ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': buy.groupingMode == 1}">
</vn-table> <span translate>{{::buy.grouping | dashIfEmpty}}</span>
</vn-card> </vn-chip>
</vn-data-viewer> </td>
<td number>{{::buy.quantity}}</td>
<td vn-two title="{{::buy.description}}">
{{::buy.description | dashIfEmpty}}
</td>
<td number>{{::buy.size}}</td>
<td vn-fetched-tags>
<div>
<vn-one title="{{::buy.name}}">{{::buy.name}}</vn-one>
<vn-one ng-if="::buy.subName">
<h3 title="{{::buy.subName}}">{{::buy.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::buy"
tabindex="-1">
</vn-fetched-tags>
</td>
<td title="{{::buy.type}}">
{{::buy.code}}
</td>
<td title="{{::item.intrastat}}">
{{::buy.intrastat}}
</td>
<td>{{::buy.origin}}</td>
<td>{{::buy.density}}</td>
<td>
<vn-check
disabled="true"
ng-model="::buy.isActive">
</vn-check>
</td>
<td>{{::buy.family}}</td>
<td>
<span
vn-click-stop="entryDescriptor.show($event, buy.entryFk)"
class="link">
{{::buy.entryFk}}
</span>
</td>
<td number>{{::buy.buyingValue | currency: 'EUR':2}}</td>
<td number>{{::buy.freightValue | currency: 'EUR':2}}</td>
<td number>{{::buy.comissionValue | currency: 'EUR':2}}</td>
<td number>{{::buy.packageValue | currency: 'EUR':2}}</td>
<td>
<vn-check
disabled="true"
ng-model="::buy.isIgnored">
</vn-check>
</td>
<td number>{{::buy.price2 | currency: 'EUR':2}}</td>
<td number>{{::buy.price3 | currency: 'EUR':2}}</td>
<td number>{{::buy.minPrice | currency: 'EUR':2}}</td>
<td>{{::buy.ektFk | dashIfEmpty}}</td>
<td>{{::buy.weight}}</td>
<td>{{::buy.packageFk}}</td>
<td>{{::buy.packingOut}}</td>
<td>{{::buy.landing | date: 'dd/MM/yyyy'}}</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<div fixed-bottom-right> <div fixed-bottom-right>
<vn-vertical style="align-items: center;"> <vn-vertical style="align-items: center;">
<vn-button class="round sm vn-mb-sm" <vn-button class="round sm vn-mb-sm"

View File

@ -5,11 +5,68 @@ import './style.scss';
export default class Controller extends Section { export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.showFields = {
id: false,
actions: false
};
this.editedColumn; this.editedColumn;
this.$checkAll = false;
this.smartTableOptions = {
activeButtons: {
search: true,
shownColumns: true,
},
columns: [
{
field: 'code',
autocomplete: {
url: 'ItemTypes',
showField: 'code',
valueField: 'code',
}
},
{
field: 'origin',
autocomplete: {
url: 'Origins',
showField: 'code',
valueField: 'code'
}
},
{
field: 'family',
autocomplete: {
url: 'ItemFamilies',
valueField: 'code',
showField: 'code'
}
},
{
field: 'intrastat',
autocomplete: {
url: 'Intrastats',
showField: 'description',
valueField: 'description'
}
},
{
field: 'packageFk',
autocomplete: {
url: 'Packagings',
showField: 'id'
}
},
{
field: 'isActive',
searchable: false
},
{
field: 'isIgnored',
searchable: false
},
{
field: 'landing',
searchable: false
}
]
};
} }
get columns() { get columns() {
@ -23,7 +80,8 @@ export default class Controller extends Section {
{field: 'description', displayName: this.$t('Description')}, {field: 'description', displayName: this.$t('Description')},
{field: 'size', displayName: this.$t('Size')}, {field: 'size', displayName: this.$t('Size')},
{field: 'density', displayName: this.$t('Density')}, {field: 'density', displayName: this.$t('Density')},
{field: 'packingOut', displayName: this.$t('PackingOut')} {field: 'packingOut', displayName: this.$t('PackingOut')},
{field: 'landing', displayName: this.$t('Landing')}
]; ];
return this._columns; return this._columns;
@ -33,21 +91,56 @@ export default class Controller extends Section {
const buys = this.$.model.data || []; const buys = this.$.model.data || [];
const checkedBuys = []; const checkedBuys = [];
for (let buy of buys) { for (let buy of buys) {
if (buy.checked) if (buy.$checked)
checkedBuys.push(buy); checkedBuys.push(buy);
} }
return checkedBuys; return checkedBuys;
} }
uncheck() { exprBuilder(param, value) {
const lines = this.checked; switch (param) {
for (let line of lines) { case 'id':
if (line.checked) case 'size':
line.checked = false; case 'density':
case 'isActive':
case 'family':
case 'minPrice':
case 'packingOut':
return {[`i.${param}`]: value};
case 'name':
case 'description':
return {[`i.${param}`]: {like: `%${value}%`}};
case 'code':
return {'it.code': value};
case 'intrastat':
return {'intr.description': value};
case 'origin':
return {'ori.code': value};
case 'landing':
return {[`lb.${param}`]: value};
case 'packing':
case 'grouping':
case 'quantity':
case 'entryFk':
case 'buyingValue':
case 'freightValue':
case 'comissionValue':
case 'packageValue':
case 'isIgnored':
case 'price2':
case 'price3':
case 'ektFk':
case 'weight':
case 'packageFk':
return {[`b.${param}`]: value};
} }
} }
uncheck() {
this.checkAll = false;
}
get totalChecked() { get totalChecked() {
return this.checked.length; return this.checked.length;
} }
@ -57,7 +150,7 @@ export default class Controller extends Section {
for (let row of this.checked) for (let row of this.checked)
rowsToEdit.push({id: row.id, itemFk: row.itemFk}); rowsToEdit.push({id: row.id, itemFk: row.itemFk});
let data = { const data = {
field: this.editedColumn.field, field: this.editedColumn.field,
newValue: this.editedColumn.newValue, newValue: this.editedColumn.newValue,
lines: rowsToEdit lines: rowsToEdit

View File

@ -7,9 +7,9 @@ describe('Entry', () => {
beforeEach(ngModule('entry')); beforeEach(ngModule('entry'));
beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpBackend_) => { beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
let $element = $compile('<vn-entry-latest-buys></vn-entry-latest-buys')($rootScope); const $element = angular.element('<vn-entry-latest-buys></vn-entry-latest-buys');
controller = $componentController('vnEntryLatestBuys', {$element}); controller = $componentController('vnEntryLatestBuys', {$element});
controller.$ = { controller.$ = {
model: {refresh: () => {}}, model: {refresh: () => {}},
@ -31,10 +31,10 @@ describe('Entry', () => {
describe('get checked', () => { describe('get checked', () => {
it(`should return a set of checked lines`, () => { it(`should return a set of checked lines`, () => {
controller.$.model.data = [ controller.$.model.data = [
{checked: true, id: 1}, {$checked: true, id: 1},
{checked: true, id: 2}, {$checked: true, id: 2},
{checked: true, id: 3}, {$checked: true, id: 3},
{checked: false, id: 4}, {$checked: false, id: 4},
]; ];
let result = controller.checked; let result = controller.checked;
@ -43,38 +43,10 @@ describe('Entry', () => {
}); });
}); });
describe('uncheck()', () => {
it(`should clear the selection of lines on the controller`, () => {
controller.$.model.data = [
{checked: true, id: 1},
{checked: true, id: 2},
{checked: true, id: 3},
{checked: false, id: 4},
];
let result = controller.checked;
expect(result.length).toEqual(3);
controller.uncheck();
result = controller.checked;
expect(result.length).toEqual(0);
});
});
describe('onEditAccept()', () => { describe('onEditAccept()', () => {
it(`should perform a query to update columns`, () => { it(`should perform a query to update columns`, () => {
$httpBackend.whenGET('UserConfigViews/getConfig?tableCode=latestBuys').respond([]);
$httpBackend.whenGET('Buys/latestBuysFilter?filter=%7B%22limit%22:20%7D').respond([
{entryFk: 1},
{entryFk: 2},
{entryFk: 3},
{entryFk: 4}
]);
controller.editedColumn = {field: 'my field', newValue: 'the new value'}; controller.editedColumn = {field: 'my field', newValue: 'the new value'};
let query = 'Buys/editLatestBuys'; const query = 'Buys/editLatestBuys';
$httpBackend.expectPOST(query).respond(); $httpBackend.expectPOST(query).respond();
controller.onEditAccept(); controller.onEditAccept();

View File

@ -4,6 +4,8 @@ Freight value: Porte
Commission value: Comisión Commission value: Comisión
Package value: Embalaje Package value: Embalaje
Is ignored: Ignorado Is ignored: Ignorado
Is visible: Visible
Is floramondo: Floramondo
Grouping price: Precio grouping Grouping price: Precio grouping
Packing price: Precio packing Packing price: Precio packing
Min price: Precio min Min price: Precio min
@ -14,4 +16,4 @@ Field to edit: Campo a editar
PackageName: Cubo PackageName: Cubo
Edit: Editar Edit: Editar
buy(s): compra(s) buy(s): compra(s)
PackingOut: Packing envíos Package out: Embalaje envíos

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