Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2544-supplier_basicData
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Bernat Exposito 2020-11-05 10:50:21 +01:00
commit 0e3d636845
94 changed files with 40696 additions and 4130 deletions

View File

@ -2,15 +2,11 @@ const app = require('vn-loopback/server/server');
describe('campaign upcoming()', () => {
it('should return the upcoming campaign but from the last year', async() => {
let response = await app.models.Campaign.upcoming();
const lastYearDate = new Date();
lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);
const lastYear = lastYearDate.getFullYear();
const response = await app.models.Campaign.upcoming();
const campaignDated = response.dated;
const campaignYear = campaignDated.getFullYear();
const now = new Date();
expect(campaignYear).toEqual(lastYear);
expect(campaignDated).toEqual(jasmine.any(Date));
expect(campaignDated).toBeLessThanOrEqual(now);
});
});

View File

@ -56,6 +56,9 @@
"Sip": {
"dataSource": "vn"
},
"SageWithholding": {
"dataSource": "vn"
},
"UserConfigView": {
"dataSource": "vn"
},

View File

@ -9,7 +9,7 @@
"properties": {
"id": {
"type": "number",
"required": true
"id": true
},
"name": {
"type": "string",
@ -56,7 +56,8 @@
"roles": {
"type": "hasMany",
"model": "RoleRole",
"foreignKey": "role"
"foreignKey": "role",
"primaryKey": "roleFk"
},
"emailUser": {
"type": "hasOne",

View File

@ -0,0 +1,33 @@
{
"name": "SageWithholding",
"base": "VnModel",
"options": {
"mysql": {
"table": "sage.TiposRetencion"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier",
"mysql": {
"columnName": "CodigoRetencion"
}
},
"withholding": {
"type": "string",
"mysql": {
"columnName": "Retencion"
}
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -0,0 +1,9 @@
ALTER TABLE `vn`.`observationType`
ADD COLUMN `code` VARCHAR(45) NOT NULL AFTER `description`;
UPDATE `vn`.`observationType` SET `code` = 'itemPicker' WHERE (`id` = '1');
UPDATE `vn`.`observationType` SET `code` = 'packager' WHERE (`id` = '2');
UPDATE `vn`.`observationType` SET `code` = 'salesPerson' WHERE (`id` = '4');
UPDATE `vn`.`observationType` SET `code` = 'administrative' WHERE (`id` = '5');
UPDATE `vn`.`observationType` SET `code` = 'weight' WHERE (`id` = '6');
UPDATE `vn`.`observationType` SET `code` = 'delivery' WHERE (`id` = '3');

View File

@ -0,0 +1,7 @@
ALTER TABLE `account`.`roleRole`
ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
ADD PRIMARY KEY (`id`);
UPDATE `account`.`role` SET id = 100 WHERE `name` = 'root';
CALL account.role_sync;

View File

@ -0,0 +1,503 @@
DROP PROCEDURE IF EXISTS account.role_syncPrivileges;
DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `account`.`role_syncPrivileges`()
BEGIN
/**
* Synchronizes permissions of MySQL role users based on role hierarchy.
* The computed role users of permission mix will be named according to
* pattern z-[role_name].
*
* If any@localhost user exists, it will be taken as a template for basic
* attributes.
*
* Warning! This procedure should only be called when MySQL privileges
* are modified. If role hierarchy is modified, you must call the role_sync()
* procedure wich calls this internally.
*/
DECLARE vIsMysql BOOL DEFAULT VERSION() NOT LIKE '%MariaDB%';
DECLARE vVersion INT DEFAULT SUBSTRING_INDEX(VERSION(), '.', 1);
DECLARE vTplUser VARCHAR(255) DEFAULT 'any';
DECLARE vTplHost VARCHAR(255) DEFAULT '%';
DECLARE vRoleHost VARCHAR(255) DEFAULT 'localhost';
DECLARE vAllHost VARCHAR(255) DEFAULT '%';
DECLARE vPrefix VARCHAR(2) DEFAULT 'z-';
DECLARE vPrefixedLike VARCHAR(255);
DECLARE vPassword VARCHAR(255) DEFAULT '';
-- Deletes computed role users
SET vPrefixedLike = CONCAT(vPrefix, '%');
IF vIsMysql THEN
DELETE FROM mysql.user
WHERE `User` LIKE vPrefixedLike;
ELSE
DELETE FROM mysql.global_priv
WHERE `User` LIKE vPrefixedLike;
END IF;
DELETE FROM mysql.db
WHERE `User` LIKE vPrefixedLike;
DELETE FROM mysql.tables_priv
WHERE `User` LIKE vPrefixedLike;
DELETE FROM mysql.columns_priv
WHERE `User` LIKE vPrefixedLike;
DELETE FROM mysql.procs_priv
WHERE `User` LIKE vPrefixedLike;
DELETE FROM mysql.proxies_priv
WHERE `Proxied_user` LIKE vPrefixedLike;
-- Temporary tables
DROP TEMPORARY TABLE IF EXISTS tRole;
CREATE TEMPORARY TABLE tRole
(INDEX (id))
ENGINE = MEMORY
SELECT
id,
`name` role,
CONCAT(vPrefix, `name`) prefixedRole
FROM role
WHERE hasLogin;
DROP TEMPORARY TABLE IF EXISTS tRoleInherit;
CREATE TEMPORARY TABLE tRoleInherit
(INDEX (inheritsFrom))
ENGINE = MEMORY
SELECT
r.prefixedRole,
ri.`name` inheritsFrom
FROM tRole r
JOIN roleRole rr ON rr.role = r.id
JOIN role ri ON ri.id = rr.inheritsFrom;
-- Recreate role users
IF vIsMysql THEN
DROP TEMPORARY TABLE IF EXISTS tUser;
CREATE TEMPORARY TABLE tUser
SELECT
r.prefixedRole `User`,
vTplHost `Host`,
IFNULL(t.`authentication_string`,
'') `authentication_string`,
IFNULL(t.`plugin`,
'mysql_native_password') `plugin`,
IFNULL(IF('' != u.`ssl_type`,
u.`ssl_type`, t.`ssl_type`),
'') `ssl_type`,
IFNULL(IF('' != u.`ssl_cipher`,
u.`ssl_cipher`, t.`ssl_cipher`),
'') `ssl_cipher`,
IFNULL(IF('' != u.`x509_issuer`,
u.`x509_issuer`, t.`x509_issuer`),
'') `x509_issuer`,
IFNULL(IF('' != u.`x509_subject`,
u.`x509_subject`, t.`x509_subject`),
'') `x509_subject`,
IFNULL(IF(0 != u.`max_questions`,
u.`max_questions`, t.`max_questions`),
0) `max_questions`,
IFNULL(IF(0 != u.`max_updates`,
u.`max_updates`, t.`max_updates`),
0) `max_updates`,
IFNULL(IF(0 != u.`max_connections`,
u.`max_connections`, t.`max_connections`),
0) `max_connections`,
IFNULL(IF(0 != u.`max_user_connections`,
u.`max_user_connections`, t.`max_user_connections`),
0) `max_user_connections`
FROM tRole r
LEFT JOIN mysql.user t
ON t.`User` = vTplUser
AND t.`Host` = vRoleHost
LEFT JOIN mysql.user u
ON u.`User` = r.role
AND u.`Host` = vRoleHost;
IF vVersion <= 5 THEN
SELECT `Password` INTO vPassword
FROM mysql.user
WHERE `User` = vTplUser
AND `Host` = vRoleHost;
INSERT INTO mysql.user (
`User`,
`Host`,
`Password`,
`authentication_string`,
`plugin`,
`ssl_type`,
`ssl_cipher`,
`x509_issuer`,
`x509_subject`,
`max_questions`,
`max_updates`,
`max_connections`,
`max_user_connections`
)
SELECT
`User`,
`Host`,
vPassword,
`authentication_string`,
`plugin`,
`ssl_type`,
`ssl_cipher`,
`x509_issuer`,
`x509_subject`,
`max_questions`,
`max_updates`,
`max_connections`,
`max_user_connections`
FROM tUser;
ELSE
INSERT INTO mysql.user (
`User`,
`Host`,
`authentication_string`,
`plugin`,
`ssl_type`,
`ssl_cipher`,
`x509_issuer`,
`x509_subject`,
`max_questions`,
`max_updates`,
`max_connections`,
`max_user_connections`
)
SELECT
`User`,
`Host`,
`authentication_string`,
`plugin`,
`ssl_type`,
`ssl_cipher`,
`x509_issuer`,
`x509_subject`,
`max_questions`,
`max_updates`,
`max_connections`,
`max_user_connections`
FROM tUser;
END IF;
DROP TEMPORARY TABLE IF EXISTS tUser;
ELSE
INSERT INTO mysql.global_priv (
`User`,
`Host`,
`Priv`
)
SELECT
r.prefixedRole,
vTplHost,
JSON_MERGE_PATCH(
IFNULL(t.`Priv`, '{}'),
IFNULL(u.`Priv`, '{}'),
JSON_OBJECT(
'mysql_old_password', JSON_VALUE(t.`Priv`, '$.mysql_old_password'),
'mysql_native_password', JSON_VALUE(t.`Priv`, '$.mysql_native_password'),
'authentication_string', JSON_VALUE(t.`Priv`, '$.authentication_string')
)
)
FROM tRole r
LEFT JOIN mysql.global_priv t
ON t.`User` = vTplUser
AND t.`Host` = vRoleHost
LEFT JOIN mysql.global_priv u
ON u.`User` = r.role
AND u.`Host` = vRoleHost;
END IF;
INSERT INTO mysql.proxies_priv (
`User`,
`Host`,
`Proxied_user`,
`Proxied_host`,
`Grantor`
)
SELECT
'',
vAllHost,
prefixedRole,
vTplHost,
CONCAT(prefixedRole, '@', vTplHost)
FROM tRole;
-- Copies global privileges
DROP TEMPORARY TABLE IF EXISTS tUserPriv;
IF vIsMysql THEN
CREATE TEMPORARY TABLE tUserPriv
(INDEX (prefixedRole))
ENGINE = MEMORY
SELECT
r.prefixedRole,
MAX(u.`Select_priv`) `Select_priv`,
MAX(u.`Insert_priv`) `Insert_priv`,
MAX(u.`Update_priv`) `Update_priv`,
MAX(u.`Delete_priv`) `Delete_priv`,
MAX(u.`Create_priv`) `Create_priv`,
MAX(u.`Drop_priv`) `Drop_priv`,
MAX(u.`Reload_priv`) `Reload_priv`,
MAX(u.`Shutdown_priv`) `Shutdown_priv`,
MAX(u.`Process_priv`) `Process_priv`,
MAX(u.`File_priv`) `File_priv`,
MAX(u.`Grant_priv`) `Grant_priv`,
MAX(u.`References_priv`) `References_priv`,
MAX(u.`Index_priv`) `Index_priv`,
MAX(u.`Alter_priv`) `Alter_priv`,
MAX(u.`Show_db_priv`) `Show_db_priv`,
MAX(u.`Super_priv`) `Super_priv`,
MAX(u.`Create_tmp_table_priv`) `Create_tmp_table_priv`,
MAX(u.`Lock_tables_priv`) `Lock_tables_priv`,
MAX(u.`Execute_priv`) `Execute_priv`,
MAX(u.`Repl_slave_priv`) `Repl_slave_priv`,
MAX(u.`Repl_client_priv`) `Repl_client_priv`,
MAX(u.`Create_view_priv`) `Create_view_priv`,
MAX(u.`Show_view_priv`) `Show_view_priv`,
MAX(u.`Create_routine_priv`) `Create_routine_priv`,
MAX(u.`Alter_routine_priv`) `Alter_routine_priv`,
MAX(u.`Create_user_priv`) `Create_user_priv`,
MAX(u.`Event_priv`) `Event_priv`,
MAX(u.`Trigger_priv`) `Trigger_priv`,
MAX(u.`Create_tablespace_priv`) `Create_tablespace_priv`
FROM tRoleInherit r
JOIN mysql.user u
ON u.`User` = r.inheritsFrom
AND u.`Host`= vRoleHost
GROUP BY r.prefixedRole;
UPDATE mysql.user u
JOIN tUserPriv t
ON u.`User` = t.prefixedRole
AND u.`Host` = vTplHost
SET
u.`Select_priv`
= t.`Select_priv`,
u.`Insert_priv`
= t.`Insert_priv`,
u.`Update_priv`
= t.`Update_priv`,
u.`Delete_priv`
= t.`Delete_priv`,
u.`Create_priv`
= t.`Create_priv`,
u.`Drop_priv`
= t.`Drop_priv`,
u.`Reload_priv`
= t.`Reload_priv`,
u.`Shutdown_priv`
= t.`Shutdown_priv`,
u.`Process_priv`
= t.`Process_priv`,
u.`File_priv`
= t.`File_priv`,
u.`Grant_priv`
= t.`Grant_priv`,
u.`References_priv`
= t.`References_priv`,
u.`Index_priv`
= t.`Index_priv`,
u.`Alter_priv`
= t.`Alter_priv`,
u.`Show_db_priv`
= t.`Show_db_priv`,
u.`Super_priv`
= t.`Super_priv`,
u.`Create_tmp_table_priv`
= t.`Create_tmp_table_priv`,
u.`Lock_tables_priv`
= t.`Lock_tables_priv`,
u.`Execute_priv`
= t.`Execute_priv`,
u.`Repl_slave_priv`
= t.`Repl_slave_priv`,
u.`Repl_client_priv`
= t.`Repl_client_priv`,
u.`Create_view_priv`
= t.`Create_view_priv`,
u.`Show_view_priv`
= t.`Show_view_priv`,
u.`Create_routine_priv`
= t.`Create_routine_priv`,
u.`Alter_routine_priv`
= t.`Alter_routine_priv`,
u.`Create_user_priv`
= t.`Create_user_priv`,
u.`Event_priv`
= t.`Event_priv`,
u.`Trigger_priv`
= t.`Trigger_priv`,
u.`Create_tablespace_priv`
= t.`Create_tablespace_priv`;
ELSE
CREATE TEMPORARY TABLE tUserPriv
(INDEX (prefixedRole))
SELECT
r.prefixedRole,
BIT_OR(JSON_VALUE(p.`Priv`, '$.access')) access
FROM tRoleInherit r
JOIN mysql.global_priv p
ON p.`User` = r.inheritsFrom
AND p.`Host`= vRoleHost
GROUP BY r.prefixedRole;
UPDATE mysql.global_priv p
JOIN tUserPriv t
ON p.`User` = t.prefixedRole
AND p.`Host` = vTplHost
SET
p.`Priv` = JSON_SET(p.`Priv`, '$.access', t.access);
END IF;
DROP TEMPORARY TABLE tUserPriv;
-- Copy schema level privileges
INSERT INTO mysql.db (
`User`,
`Host`,
`Db`,
`Select_priv`,
`Insert_priv`,
`Update_priv`,
`Delete_priv`,
`Create_priv`,
`Drop_priv`,
`Grant_priv`,
`References_priv`,
`Index_priv`,
`Alter_priv`,
`Create_tmp_table_priv`,
`Lock_tables_priv`,
`Create_view_priv`,
`Show_view_priv`,
`Create_routine_priv`,
`Alter_routine_priv`,
`Execute_priv`,
`Event_priv`,
`Trigger_priv`
)
SELECT
r.prefixedRole,
vTplHost,
t.`Db`,
MAX(t.`Select_priv`),
MAX(t.`Insert_priv`),
MAX(t.`Update_priv`),
MAX(t.`Delete_priv`),
MAX(t.`Create_priv`),
MAX(t.`Drop_priv`),
MAX(t.`Grant_priv`),
MAX(t.`References_priv`),
MAX(t.`Index_priv`),
MAX(t.`Alter_priv`),
MAX(t.`Create_tmp_table_priv`),
MAX(t.`Lock_tables_priv`),
MAX(t.`Create_view_priv`),
MAX(t.`Show_view_priv`),
MAX(t.`Create_routine_priv`),
MAX(t.`Alter_routine_priv`),
MAX(t.`Execute_priv`),
MAX(t.`Event_priv`),
MAX(t.`Trigger_priv`)
FROM tRoleInherit r
JOIN mysql.db t
ON t.`User` = r.inheritsFrom
AND t.`Host`= vRoleHost
GROUP BY r.prefixedRole, t.`Db`;
-- Copy table level privileges
INSERT INTO mysql.tables_priv (
`User`,
`Host`,
`Db`,
`Table_name`,
`Grantor`,
`Timestamp`,
`Table_priv`,
`Column_priv`
)
SELECT
r.prefixedRole,
vTplHost,
t.`Db`,
t.`Table_name`,
t.`Grantor`,
MAX(t.`Timestamp`),
IFNULL(GROUP_CONCAT(NULLIF(t.`Table_priv`, '')), ''),
IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '')
FROM tRoleInherit r
JOIN mysql.tables_priv t
ON t.`User` = r.inheritsFrom
AND t.`Host`= vRoleHost
GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`;
-- Copy column level privileges
INSERT INTO mysql.columns_priv (
`User`,
`Host`,
`Db`,
`Table_name`,
`Column_name`,
`Timestamp`,
`Column_priv`
)
SELECT
r.prefixedRole,
vTplHost,
t.`Db`,
t.`Table_name`,
t.`Column_name`,
MAX(t.`Timestamp`),
IFNULL(GROUP_CONCAT(NULLIF(t.`Column_priv`, '')), '')
FROM tRoleInherit r
JOIN mysql.columns_priv t
ON t.`User` = r.inheritsFrom
AND t.`Host`= vRoleHost
GROUP BY r.prefixedRole, t.`Db`, t.`Table_name`, t.`Column_name`;
-- Copy routine privileges
INSERT IGNORE INTO mysql.procs_priv (
`User`,
`Host`,
`Db`,
`Routine_name`,
`Routine_type`,
`Grantor`,
`Timestamp`,
`Proc_priv`
)
SELECT
r.prefixedRole,
vTplHost,
t.`Db`,
t.`Routine_name`,
t.`Routine_type`,
t.`Grantor`,
t.`Timestamp`,
t.`Proc_priv`
FROM tRoleInherit r
JOIN mysql.procs_priv t
ON t.`User` = r.inheritsFrom
AND t.`Host`= vRoleHost;
-- Free memory
DROP TEMPORARY TABLE
tRole,
tRoleInherit;
FLUSH PRIVILEGES;
END$$
DELIMITER ;

View File

@ -0,0 +1,10 @@
ALTER TABLE account.sambaConfig ADD adUser VARCHAR(255) DEFAULT NULL NULL COMMENT 'Active directory user';
ALTER TABLE account.sambaConfig ADD adPassword varchar(255) DEFAULT NULL NULL COMMENT 'Active directory password';
ALTER TABLE account.sambaConfig ADD userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN for users';
ALTER TABLE account.sambaConfig DROP COLUMN uidBase;
ALTER TABLE account.sambaConfig CHANGE sshPass sshPassword varchar(255) DEFAULT NULL NULL COMMENT 'The SSH password';
ALTER TABLE account.ldapConfig DROP COLUMN `filter`;
ALTER TABLE account.ldapConfig CHANGE baseDn userDn varchar(255) DEFAULT NULL NULL COMMENT 'The base DN to do the query';
ALTER TABLE account.ldapConfig CHANGE host server varchar(255) NOT NULL COMMENT 'The hostname of LDAP server';
ALTER TABLE account.ldapConfig MODIFY COLUMN password varchar(255) NOT NULL COMMENT 'The LDAP password';

View File

@ -0,0 +1,24 @@
CREATE TABLE `vn`.`supplierContact` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`supplierFk` INT(11) NULL DEFAULT NULL,
`phone` VARCHAR(16) NULL DEFAULT NULL,
`mobile` VARCHAR(16) NULL DEFAULT NULL,
`email` VARCHAR(255) NULL DEFAULT NULL,
`observation` TEXT NULL DEFAULT NULL,
`name` VARCHAR(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`))
ENGINE = InnoDB;
ALTER TABLE `vn`.`supplierContact`
ADD CONSTRAINT `supplier_id`
FOREIGN KEY (`supplierFk`)
REFERENCES `vn`.`supplier` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE;
INSERT INTO vn.supplierContact(supplierFk,phone,mobile,email,observation,`name`)
SELECT r.Id_Proveedor,c.Telefono,c.Movil,c.email,c.Notas,concat(c.Nombre," ", IFNULL(c.Apellidos,""))
FROM vn2008.Contactos c
JOIN vn2008.Relaciones r ON r.Id_Contacto = c.Id_Contacto
JOIN vn.supplier s ON s.id = r.Id_Proveedor;

View File

@ -0,0 +1,25 @@
DROP TRIGGER IF EXISTS `vn`.`ticket_afterUpdate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `ticket_afterUpdate`
AFTER UPDATE ON `ticket`
FOR EACH ROW
BEGIN
IF !(NEW.id <=> OLD.id)
OR !(NEW.warehouseFk <=> OLD.warehouseFk)
OR !(NEW.shipped <=> OLD.shipped) THEN
CALL stock.log_add('ticket', NEW.id, OLD.id);
END IF;
IF NEW.clientFk = 2067 AND !(NEW.clientFk <=> OLD.clientFk) THEN
-- Fallo que se insertan no se sabe como tickets en este cliente
INSERT INTO vn.mail SET
`sender` = 'jgallego@verdnatura.es',
`replyTo` = 'jgallego@verdnatura.es',
`subject` = 'Modificado ticket al cliente 2067',
`body` = CONCAT(account.myUserGetName(), ' ha modificado el ticket ',
NEW.id);
END IF;
END$$
DELIMITER ;

View File

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

View File

@ -0,0 +1,43 @@
DROP PROCEDURE IF EXISTS `vn`.`timeControl_calculate`;
DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`timeControl_calculate`(vDatedFrom DATETIME, vDatedTo DATETIME)
BEGIN
SET @vIsOdd := TRUE;
SET @vUser := NULL;
SET @vDated := NULL;
DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate;
CREATE TEMPORARY TABLE tmp.timeControlCalculate
SELECT
userFk,
dated,
IF( timeWork >= 18000, @timeWork:=timeWork + 1200, @timeWork:=timeWork) timeWorkSeconds,
SEC_TO_TIME(@timeWork ) timeWorkSexagesimal,
@timeWork / 3600 timeWorkDecimal,
timed
FROM (SELECT SUM(timeWork) timeWork,
userFk,
dated,
GROUP_CONCAT(DATE_FORMAT(sub.timed,"%H:%i") ORDER BY sub.timed ASC SEPARATOR ' - ') timed
FROM (SELECT IF(@vUser = wtc.userFk, @vUser :=@vUser, @vUser := wtc.userFk),
IF(@vIsOdd, @vIsOdd := FALSE, @vIsOdd := TRUE),
IF(direction='in', @vIsOdd := TRUE, @vIsOdd := @vIsOdd),
IF(@vIsOdd, @vLastTimed:=UNIX_TIMESTAMP(timed),@vLastTimed:=@vLastTimed),
IF(@vIsOdd, 0, UNIX_TIMESTAMP(timed)-@vLastTimed) timeWork,
IF(direction='in', @vDated := DATE(wtc.timed), @vDated :=@vDated) dated,
wtc.timed timed,
wtc.userFk,
direction
FROM (SELECT DISTINCT(wtc.id), wtc.userFk, wtc.timed, wtc.direction
FROM workerTimeControl wtc
JOIN tmp.`user` w ON w.userFk = wtc.userFk
WHERE wtc.timed BETWEEN vDatedFrom AND vDatedTo
ORDER BY userFk, timed ASC
) wtc
WHERE wtc.timed BETWEEN vDatedFrom AND vDatedTo
) sub
GROUP BY userFk, dated
)sub2;
END$$
DELIMITER ;

View File

@ -604,6 +604,24 @@ INSERT INTO `TiposTransacciones` VALUES (1,'Rég.general/Oper.interiores bienes
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
--
-- Dumping data for table `TiposRetencion`
--
LOCK TABLES `TiposRetencion` WRITE;
/*!40000 ALTER TABLE `TiposRetencion` DISABLE KEYS */;
INSERT INTO `TiposRetencion` (`CodigoRetencion`, `Retencion`, `PorcentajeRetencion`, `CuentaCargo`, `CuentaAbono`, `ClaveIrpf`, `CuentaCargoANT_`, `CuentaAbonoANT_`, `IdTipoRetencion`) VALUES
(1, 'RETENCION ESTIMACION OBJETIVA', '1.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, '03811652-0F3A-44A1-AE1C-B19624525D7F'),
(2, 'ACTIVIDADES AGRICOLAS O GANADERAS', '2.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, 'F3F91EF3-FED6-444D-B03C-75B639D13FB4'),
(9, 'ACTIVIDADES PROFESIONALES 2 PRIMEROS AÑOS', '9.0000000000', '4730000000', '4751000000', NULL, NULL, NULL, '73F95642-E951-4C91-970A-60C503A4792B'),
(15, 'ACTIVIDADES PROFESIONALES', '15.0000000000', '4730000000', '4751000000', '6', NULL, NULL, 'F6BDE0EE-3B01-4023-8FFF-A73AE9AC50D7'),
(19, 'ARRENDAMIENTO Y SUBARRENDAMIENTO', '19.0000000000', '4730000000', '4751000000', '8', NULL, NULL, '09B033AE-16E5-4057-8D4A-A7710C8A4FB9');
/*!40000 ALTER TABLE `TiposRetencion` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;

View File

@ -304,36 +304,36 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(10, 'Jessica Jones', 'NYCC 2015 Poster', 'Silla', 46460, 1, 1111111111, 222222222, 1, 110, 2, NULL, NULL, 0, 1),
(11, 'Missing', 'The space', 'Silla', 46460, 1, 1111111111, 222222222, 1, 111, 10, NULL, NULL, 0, 1),
(12, 'Trash', 'New York city', 'Silla', 46460, 1, 1111111111, 222222222, 1, 112, 10, NULL, NULL, 0, 1),
(101, 'address 01', 'Somewhere in Thailand', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(102, 'address 02', 'Somewhere in Poland', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(103, 'address 03', 'Somewhere in Japan', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(104, 'address 04', 'Somewhere in Spain', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(105, 'address 05', 'Somewhere in Potugal', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(106, 'address 06', 'Somewhere in UK', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(107, 'address 07', 'Somewhere in Valencia', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(108, 'address 08', 'Somewhere in Silla', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(109, 'address 09', 'Somewhere in London', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(110, 'address 10', 'Somewhere in Algemesi', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(111, 'address 11', 'Somewhere in Carlet', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(112, 'address 12', 'Somewhere in Campanar', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(113, 'address 13', 'Somewhere in Malilla', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(114, 'address 14', 'Somewhere in France', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(115, 'address 15', 'Somewhere in Birmingham', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(116, 'address 16', 'Somewhere in Scotland', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(117, 'address 17', 'Somewhere in nowhere', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(118, 'address 18', 'Somewhere over the rainbow', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(119, 'address 19', 'Somewhere in Alberic', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(120, 'address 20', 'Somewhere in Montortal', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(121, 'address 21', 'the bat cave', 'Silla', 46460, 1, 1111111111, 222222222, 1, 101, 2, NULL, NULL, 0, 0),
(122, 'address 22', 'NY roofs', 'Silla', 46460, 1, 1111111111, 222222222, 1, 102, 2, NULL, NULL, 0, 0),
(123, 'address 23', 'The phone box', 'Silla', 46460, 1, 1111111111, 222222222, 1, 103, 2, NULL, NULL, 0, 0),
(124, 'address 24', 'Stark tower Silla', 'Silla', 46460, 1, 1111111111, 222222222, 1, 104, 2, NULL, NULL, 0, 0),
(125, 'address 25', 'The plastic cell', 'Silla', 46460, 1, 1111111111, 222222222, 1, 105, 2, NULL, NULL, 0, 0),
(126, 'address 26', 'Many places', 'Silla', 46460, 1, 1111111111, 222222222, 1, 106, 2, NULL, NULL, 0, 0),
(127, 'address 27', 'Your pocket', 'Silla', 46460, 1, 1111111111, 222222222, 1, 107, 2, NULL, NULL, 0, 0),
(128, 'address 28', 'Cerebro', 'Silla', 46460, 1, 1111111111, 222222222, 1, 108, 2, NULL, NULL, 0, 0),
(129, 'address 29', 'Luke Cages Bar', 'Silla', 46460, 1, 1111111111, 222222222, 1, 110, 2, NULL, NULL, 0, 0),
(130, 'address 30', 'Non valid address', 'Silla', 46460, 1, 1111111111, 222222222, 0, 101, 2, NULL, NULL, 0, 0);
(101, 'Somewhere in Thailand', 'address 01', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(102, 'Somewhere in Poland', 'address 02', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(103, 'Somewhere in Japan', 'address 03', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(104, 'Somewhere in Spain', 'address 04', 'Silla', 46460, 1, 3333333333, 444444444, 1, 109, 2, NULL, NULL, 0, 0),
(105, 'Somewhere in Potugal', 'address 05', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(106, 'Somewhere in UK', 'address 06', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(107, 'Somewhere in Valencia', 'address 07', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(108, 'Somewhere in Silla', 'address 08', 'Silla', 46460, 1, 5555555555, 666666666, 1, 109, 2, NULL, NULL, 0, 0),
(109, 'Somewhere in London', 'address 09', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(110, 'Somewhere in Algemesi', 'address 10', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(111, 'Somewhere in Carlet', 'address 11', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(112, 'Somewhere in Campanar', 'address 12', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(113, 'Somewhere in Malilla', 'address 13', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(114, 'Somewhere in France', 'address 14', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(115, 'Somewhere in Birmingham', 'address 15', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(116, 'Somewhere in Scotland', 'address 16', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(117, 'Somewhere in nowhere', 'address 17', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(118, 'Somewhere over the rainbow', 'address 18', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(119, 'Somewhere in Alberic', 'address 19', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(120, 'Somewhere in Montortal', 'address 20', 'Silla', 46460, 1, 1111111111, 222222222, 1, 109, 2, NULL, NULL, 0, 0),
(121, 'the bat cave', 'address 21', 'Silla', 46460, 1, 1111111111, 222222222, 1, 101, 2, NULL, NULL, 0, 0),
(122, 'NY roofs', 'address 22', 'Silla', 46460, 1, 1111111111, 222222222, 1, 102, 2, NULL, NULL, 0, 0),
(123, 'The phone box', 'address 23', 'Silla', 46460, 1, 1111111111, 222222222, 1, 103, 2, NULL, NULL, 0, 0),
(124, 'Stark tower Silla', 'address 24', 'Silla', 46460, 1, 1111111111, 222222222, 1, 104, 2, NULL, NULL, 0, 0),
(125, 'The plastic cell', 'address 25', 'Silla', 46460, 1, 1111111111, 222222222, 1, 105, 2, NULL, NULL, 0, 0),
(126, 'Many places', 'address 26', 'Silla', 46460, 1, 1111111111, 222222222, 1, 106, 2, NULL, NULL, 0, 0),
(127, 'Your pocket', 'address 27', 'Silla', 46460, 1, 1111111111, 222222222, 1, 107, 2, NULL, NULL, 0, 0),
(128, 'Cerebro', 'address 28', 'Silla', 46460, 1, 1111111111, 222222222, 1, 108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Silla', 46460, 1, 1111111111, 222222222, 1, 110, 2, NULL, NULL, 0, 0),
(130, 'Non valid address', 'address 30', 'Silla', 46460, 1, 1111111111, 222222222, 0, 101, 2, NULL, NULL, 0, 0);
INSERT INTO `vn`.`address`( `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `isActive`, `clientFk`, `agencyModeFk`, `isDefaultAddress`)
SELECT name, CONCAT(name, 'Street'), 'SILLA', 46460, 1, 1, id, 2, 1
@ -400,18 +400,20 @@ INSERT INTO `vn`.`clientObservation`(`id`, `clientFk`, `workerFk`, `text`, `crea
(9, 109, 18, 'HULK SMASH! ...', CURDATE()),
(10, 110, 18, 'They say everyone is born a hero. But if you let it, life will push you over the line until you are the villain.', CURDATE());
INSERT INTO `vn`.`observationType`(`id`,`description`)
INSERT INTO `vn`.`observationType`(`id`,`description`, `code`)
VALUES
(1,'observation one'),
(2,'observation two'),
(3,'observation three'),
(4,'comercial');
(1, 'observation one', 'observation one'),
(2, 'observation two', 'observation two'),
(3, 'observation three', 'observation three'),
(4, 'comercial', 'salesPerson'),
(5, 'delivery', 'delivery');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES
(1, 121, 1, 'under the floor'),
(2, 121, 2, 'wears leather and goes out at night'),
(3, 121, 3, 'care with the dog');
(3, 121, 3, 'care with the dog'),
(5, 122, 5, 'Delivery after 10am');
INSERT INTO `vn`.`creditClassification`(`id`, `client`, `dateStart`, `dateEnd`)
VALUES
@ -605,7 +607,8 @@ INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `des
(8, 23, 2, 'wears leather and goes out at night'),
(9, 23, 3, 'care with the dog'),
(10, 23, 4, 'Reclama ticket: 8'),
(11, 24, 4, 'Reclama ticket: 7');
(11, 24, 4, 'Reclama ticket: 7'),
(12, 11, 5, 'Delivery after 10am');
-- FIX for state hours on local, inter_afterInsert
UPDATE vncontrol.inter SET odbc_date = DATE_ADD(CURDATE(), INTERVAL -10 SECOND);
@ -1208,11 +1211,18 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`)
(104, 500),
(105, 5000);
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `transactionTypeSageFk`)
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`,`isFarmer`,`commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`)
VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, NULL, NULL),
(2, 'Farmer King', 'The farmer', 4000020002, 1, 'B22222222', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 8),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, 'C33333333', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, NULL, NULL);
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 2, 8),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, 6, 9, 3);
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES
(1, 1, 123121212, 654789123, 'supplier1@email.es', 'observation1', 'the boss'),
(2, 1, 987456132, NULL, NULL, NULL, 'the salesperson'),
(3, 2, 321654987, NULL, 'supplier2@email.es', NULL, NULL),
(4, 442, 321654987, NULL, NULL, 'observation442', NULL);
INSERT INTO `cache`.`cache_calc`(`id`, `cache_id`, `cacheName`, `params`, `last_refresh`, `expires`, `created`, `connection_id`)
VALUES

View File

@ -123,15 +123,20 @@ let actions = {
},
waitForState: async function(state) {
await this.waitFor(state => {
await this.waitForFunction(state => {
let $state = angular.element(document.body).injector().get('$state');
return !$state.transition && $state.is(state);
}, {}, state);
await this.waitForFunction(() => {
return angular.element(() => {
return true;
});
});
await this.waitForSpinnerLoad();
},
waitForTransition: async function() {
await this.waitFor(() => {
await this.waitForFunction(() => {
const $state = angular.element(document.body).injector().get('$state');
return !$state.transition;
});
@ -522,7 +527,7 @@ let actions = {
},
waitForSpinnerLoad: async function() {
await this.waitFor('vn-topbar vn-spinner', {hidden: true});
await this.waitForSelector('vn-topbar vn-spinner', {hidden: true});
},
waitForWatcherData: async function(selector) {

View File

@ -123,19 +123,19 @@ export default {
streetAddress: 'vn-textfield[ng-model="$ctrl.address.street"]',
postcode: 'vn-datalist[ng-model="$ctrl.address.postalCode"]',
city: 'vn-datalist[ng-model="$ctrl.address.city"]',
province: 'vn-autocomplete[ng-model="$ctrl.address.provinceId"]',
agency: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeId"]',
province: 'vn-autocomplete[ng-model="$ctrl.address.provinceFk"]',
agency: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeFk"]',
phone: 'vn-textfield[ng-model="$ctrl.address.phone"]',
mobileInput: 'vn-textfield[ng-model="$ctrl.address.mobile"]',
defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]',
incoterms: 'vn-autocomplete[ng-model="$ctrl.address.incotermsId"]',
addNewCustomsAgent: 'vn-client-address-create vn-autocomplete[ng-model="$ctrl.address.customsAgentId"] vn-icon-button[icon="add_circle"]',
incoterms: 'vn-autocomplete[ng-model="$ctrl.address.incotermsFk"]',
addNewCustomsAgent: 'vn-client-address-create vn-autocomplete[ng-model="$ctrl.address.customsAgentFk"] vn-icon-button[icon="add_circle"]',
newCustomsAgentFiscalID: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.nif"]',
newCustomsAgentFiscalName: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.fiscalName"]',
newCustomsAgentStreet: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.street"]',
newCustomsAgentPhone: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.phone"]',
saveNewCustomsAgentButton: 'button[response="accept"]',
customsAgent: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentId"]',
customsAgent: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentFk"]',
secondMakeDefaultStar: 'vn-client-address-index vn-card div:nth-child(2) vn-icon-button[icon="star_border"]',
firstEditAddress: 'vn-client-address-index div:nth-child(1) > a',
secondEditAddress: 'vn-client-address-index div:nth-child(2) > a',
@ -912,5 +912,31 @@ export default {
alias: 'vn-supplier-descriptor vn-label-value[label="Alias"]',
clientButton: 'vn-supplier-descriptor vn-icon[icon="person"]',
entriesButton: 'vn-supplier-descriptor vn-icon[icon="icon-entry"]',
},
supplierContact: {
anyContact: 'vn-supplier-contact > form > vn-card > div',
addNewContact: 'vn-supplier-contact vn-icon[icon="add_circle"]',
thirdContactName: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.name"]',
thirdContactPhone: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.phone"]',
thirdContactMobile: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.mobile"]',
thirdContactEmail: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.email"]',
thirdContactNotes: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.observation"]',
saveButton: 'vn-supplier-contact button[type="submit"]',
thirdContactDeleteButton: 'vn-supplier-contact div:nth-child(3) vn-icon-button[icon="delete"]'
},
supplierBasicData: {
},
supplierFiscalData: {
socialName: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.name"]',
taxNumber: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.nif"]',
account: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.account"]',
sageTaxType: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageTaxTypeFk"]',
sageWihholding: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageWithholdingFk"]',
postCode: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.postCode"]',
city: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.city"]',
province: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.provinceFk"]',
country: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.countryFk"]',
saveButton: 'vn-supplier-fiscal-data button[type="submit"]',
}
};

View File

@ -95,14 +95,14 @@ describe('Client Add address path', () => {
});
it(`should confirm the default address is the expected one`, async() => {
await page.waitForTextInElement(selectors.clientAddresses.defaultAddress, 'Somewhere in Thailand');
await page.waitForTextInElement(selectors.clientAddresses.defaultAddress, 'somewhere in new york');
const result = await page.waitToGetProperty(selectors.clientAddresses.defaultAddress, 'innerText');
expect(result).toContain('Somewhere in Thailand');
expect(result).toContain('Somewhere in New York');
});
it(`should click on the edit icon of the default address`, async() => {
await page.waitForTextInElement(selectors.clientAddresses.defaultAddress, 'Somewhere in Thailand');
await page.waitForTextInElement(selectors.clientAddresses.defaultAddress, 'somewhere in new york');
await page.waitToClick(selectors.clientAddresses.firstEditAddress);
await page.waitForState('client.card.address.edit');
});

View File

@ -57,6 +57,8 @@ describe('Client lock verified data path', () => {
});
it('should check the Verified data checkbox', async() => {
await page.autocompleteSearch(selectors.clientFiscalData.sageTax, 'operaciones no sujetas');
await page.autocompleteSearch(selectors.clientFiscalData.sageTransaction, 'regularización de inversiones');
await page.waitToClick(selectors.clientFiscalData.verifiedDataCheckbox);
await page.waitToClick(selectors.clientFiscalData.saveButton);
await page.waitToClick(selectors.globalItems.acceptButton);

View File

@ -24,7 +24,7 @@ describe('Worker calendar path', () => {
expect(result).toContain(' 5 ');
});
it('should set two days as holidays on the calendar', async() => {
it('should set two days as holidays on the calendar and check the total holidays increased by 1.5', async() => {
await page.waitToClick(selectors.workerCalendar.holidays);
await page.waitFor(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.januaryThirtyFirst);
@ -50,9 +50,8 @@ describe('Worker calendar path', () => {
await page.waitToClick(selectors.workerCalendar.halfFurlough);
await page.waitFor(reasonableTimeBetweenClicks);
await page.waitToClick(selectors.workerCalendar.mayEighth);
});
await page.waitFor(reasonableTimeBetweenClicks);
it('should check the total holidays increased by 1.5', async() => {
const result = await page.waitToGetProperty(selectors.workerCalendar.totalHolidaysUsed, 'innerText');
expect(result).toContain(' 6.5 ');

View File

@ -48,7 +48,7 @@ describe('Ticket Create new tracking state path', () => {
});
it('should now access to the create state view by clicking the create floating button', async() => {
await page.waitFor('.vn-popup', {hidden: true}),
await page.waitFor('.vn-popup', {hidden: true});
await page.waitToClick(selectors.ticketTracking.createStateButton);
await page.waitForState('ticket.card.tracking.edit');
});

View File

@ -29,13 +29,13 @@ describe('Order summary path', () => {
it('should check the summary contains the order alias', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.alias, 'innerText');
expect(result).toEqual('address 26');
expect(result).toEqual('Many places');
});
it('should check the summary contains the order consignee', async() => {
const result = await page.waitToGetProperty(selectors.orderSummary.consignee, 'innerText');
expect(result).toEqual('Many places - Silla (Province one)');
expect(result).toEqual('address 26 - Silla (Province one)');
});
it('should check the summary contains the order subtotal', async() => {

View File

@ -1,7 +1,7 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Supplier descriptor path', () => {
describe('Supplier summary & descriptor path', () => {
let browser;
let page;
@ -79,6 +79,6 @@ describe('Supplier descriptor path', () => {
});
it(`should check the client button isn't present since this supplier should not be a client`, async() => {
await page.waitForSelector(selectors.supplierDescriptor.clientButton, {hidden: true});
await page.waitForSelector(selectors.supplierDescriptor.clientButton, {visible: false});
});
});

View File

@ -0,0 +1,84 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Supplier contact path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'supplier');
await page.accessToSearchResult('1');
await page.accessToSection('supplier.card.contact');
});
afterAll(async() => {
await browser.close();
});
it('should create a new contact', async() => {
await page.waitToClick(selectors.supplierContact.addNewContact);
await page.write(selectors.supplierContact.thirdContactName, 'The tester');
await page.write(selectors.supplierContact.thirdContactPhone, '99 999 99 99');
await page.write(selectors.supplierContact.thirdContactMobile, '555 55 55 55');
await page.write(selectors.supplierContact.thirdContactEmail, 'testing@puppeteer.com');
await page.write(selectors.supplierContact.thirdContactNotes, 'the end to end integration tester');
await page.waitToClick(selectors.supplierContact.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toBe('Data saved!');
});
it(`should reload the section and count the contacts`, async() => {
await page.reloadSection('supplier.card.contact');
const result = await page.countElement(selectors.supplierContact.anyContact);
expect(result).toEqual(3);
});
it(`should check the new contact name was saved correctly`, async() => {
const result = await page.waitToGetProperty(selectors.supplierContact.thirdContactName, 'value');
expect(result).toContain('The tester');
});
it(`should check the new contact phone was saved correctly`, async() => {
const result = await page.waitToGetProperty(selectors.supplierContact.thirdContactPhone, 'value');
expect(result).toContain('99 999 99 99');
});
it(`should check the new contact mobile was saved correctly`, async() => {
const result = await page.waitToGetProperty(selectors.supplierContact.thirdContactMobile, 'value');
expect(result).toContain('555 55 55 55');
});
it(`should check the new contact email was saved correctly`, async() => {
const result = await page.waitToGetProperty(selectors.supplierContact.thirdContactEmail, 'value');
expect(result).toContain('testing@puppeteer.com');
});
it(`should check the new contact note was saved correctly`, async() => {
const result = await page.waitToGetProperty(selectors.supplierContact.thirdContactNotes, 'value');
expect(result).toContain('the end to end integration tester');
});
it(`should remove the created contact`, async() => {
await page.waitToClick(selectors.supplierContact.thirdContactDeleteButton, 'value');
await page.waitToClick(selectors.supplierContact.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toBe('Data saved!');
});
it(`should reload the section and count the amount of contacts went back to 2`, async() => {
await page.reloadSection('supplier.card.contact');
const result = await page.countElement(selectors.supplierContact.anyContact);
expect(result).toEqual(2);
});
});

View File

@ -0,0 +1,108 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Supplier fiscal data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'supplier');
await page.accessToSearchResult('2');
await page.accessToSection('supplier.card.fiscalData');
});
afterAll(async() => {
await browser.close();
});
it('should attempt to edit the fiscal data but fail as the tax number is invalid', async() => {
await page.clearInput(selectors.supplierFiscalData.city);
await page.clearInput(selectors.supplierFiscalData.province);
await page.clearInput(selectors.supplierFiscalData.country);
await page.clearInput(selectors.supplierFiscalData.postCode);
await page.write(selectors.supplierFiscalData.city, 'Valencia');
await page.clearInput(selectors.supplierFiscalData.socialName);
await page.write(selectors.supplierFiscalData.socialName, 'Farmer King SL');
await page.clearInput(selectors.supplierFiscalData.taxNumber);
await page.write(selectors.supplierFiscalData.taxNumber, 'invalid tax number');
await page.clearInput(selectors.supplierFiscalData.account);
await page.write(selectors.supplierFiscalData.account, 'edited account number');
await page.autocompleteSearch(selectors.supplierFiscalData.sageWihholding, 'retencion estimacion objetiva');
await page.autocompleteSearch(selectors.supplierFiscalData.sageTaxType, 'operaciones no sujetas');
await page.waitToClick(selectors.supplierFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toBe('Invalid Tax number');
});
it('should save the changes as the tax number is valid this time', async() => {
await page.clearInput(selectors.supplierFiscalData.taxNumber);
await page.write(selectors.supplierFiscalData.taxNumber, '12345678Z');
await page.waitToClick(selectors.supplierFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toBe('Data saved!');
});
it('should reload the section', async() => {
await page.reloadSection('supplier.card.fiscalData');
});
it('should check the socialName was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.socialName, 'value');
expect(result).toEqual('Farmer King SL');
});
it('should check the taxNumber was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.taxNumber, 'value');
expect(result).toEqual('12345678Z');
});
it('should check the account was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.account, 'value');
expect(result).toEqual('edited account number');
});
it('should check the sageWihholding was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageWihholding, 'value');
expect(result).toEqual('RETENCION ESTIMACION OBJETIVA');
});
it('should check the sageTaxType was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageTaxType, 'value');
expect(result).toEqual('Operaciones no sujetas');
});
it('should check the postCode was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.postCode, 'value');
expect(result).toEqual('46000');
});
it('should check the city was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.city, 'value');
expect(result).toEqual('Valencia');
});
it('should check the province was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.province, 'value');
expect(result).toEqual('Province one (España)');
});
it('should check the country was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.country, 'value');
expect(result).toEqual('España');
});
});

5148
front/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ let buildDir = 'dist';
let backSources = [
'!node_modules',
'!loopback/locale/*.json',
'loopback',
'modules/*/back/**',
'modules/*/back/*',

View File

@ -154,7 +154,8 @@ module.exports = function(Self) {
const showFieldNames = [
'name',
'description',
'code'
'code',
'nickname'
];
for (field of showFieldNames) {
const propField = properties && properties[field];

View File

@ -81,5 +81,8 @@
"shipped": "Shipped",
"landed": "Landed",
"addressFk": "Address",
"companyFk": "Company"
"companyFk": "Company",
"You need to fill sage information before you check verified data": "You need to fill sage information before you check verified data",
"The social name cannot be empty": "The social name cannot be empty",
"The nif cannot be empty": "The nif cannot be empty"
}

View File

@ -156,5 +156,8 @@
"shipped": "F. envío",
"landed": "F. entrega",
"addressFk": "Consignatario",
"companyFk": "Empresa"
"companyFk": "Empresa",
"The social name cannot be empty": "La razón social no puede quedar en blanco",
"The nif cannot be empty": "El NIF no puede quedar en blanco",
"You need to fill sage information before you check verified data": "Debes rellenar la información de sage antes de marcar datos comprobados"
}

View File

@ -1,4 +1,5 @@
const ldap = require('../../util/ldapjs-extra');
const SyncEngine = require('../../util/sync-engine');
module.exports = Self => {
Self.remoteMethod('sync', {
@ -10,105 +11,17 @@ module.exports = Self => {
});
Self.sync = async function() {
let $ = Self.app.models;
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'groupDn']
});
let accountConfig = await $.AccountConfig.findOne({
fields: ['idBase']
});
if (!ldapConfig) return;
// Connect
let client = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await client.bind(ldapConfig.rdn, ldapPassword);
let engine = new SyncEngine();
await engine.init(Self.app.models);
let err;
try {
// Delete roles
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: 'objectClass=posixGroup'
};
res = await client.search(ldapConfig.groupDn, opts);
let reqs = [];
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${ldapConfig.groupDn}' does not exist`);
reject(err);
});
res.on('searchEntry', e => {
reqs.push(client.del(e.object.dn));
});
res.on('end', resolve);
});
await Promise.all(reqs);
// Recreate roles
let roles = await $.Role.find({
fields: ['id', 'name']
});
let accounts = await $.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
include: {
relation: 'roles',
scope: {
fields: ['inheritsFrom']
}
}
}
}
});
let map = new Map();
for (let account of accounts) {
let user = account.user();
for (let inherit of user.roles()) {
let roleId = inherit.inheritsFrom;
if (!map.has(roleId)) map.set(roleId, []);
map.get(roleId).push(user.name);
}
}
reqs = [];
for (let role of roles) {
let newEntry = {
objectClass: ['top', 'posixGroup'],
cn: role.name,
gidNumber: accountConfig.idBase + role.id
};
let memberUid = map.get(role.id);
if (memberUid) newEntry.memberUid = memberUid;
let dn = `cn=${role.name},${ldapConfig.groupDn}`;
reqs.push(client.add(dn, newEntry));
}
await Promise.all(reqs);
await engine.syncRoles();
} catch (e) {
err = e;
}
// FIXME: Cannot disconnect, hangs on undind() call
// await client.unbind();
await engine.deinit();
if (err) throw err;
};
};

View File

@ -0,0 +1,36 @@
const SyncEngine = require('../../util/sync-engine');
module.exports = Self => {
Self.remoteMethod('syncAll', {
description: 'Synchronizes user database with LDAP and Samba',
http: {
path: `/syncAll`,
verb: 'PATCH'
}
});
Self.syncAll = async function() {
let $ = Self.app.models;
let engine = new SyncEngine();
await engine.init($);
let usersToSync = await engine.getUsers();
usersToSync = Array.from(usersToSync.values())
.sort((a, b) => a.localeCompare(b));
for (let user of usersToSync) {
try {
console.log(`Synchronizing user '${user}'`);
await engine.sync(user);
console.log(` -> '${user}' sinchronized`);
} catch (err) {
console.error(` -> '${user}' synchronization error:`, err.message);
}
}
await engine.deinit();
await $.RoleInherit.sync();
};
};

View File

@ -1,7 +1,5 @@
const ldap = require('../../util/ldapjs-extra');
const nthash = require('smbhash').nthash;
const ssh = require('node-ssh');
const crypto = require('crypto');
const SyncEngine = require('../../util/sync-engine');
module.exports = Self => {
Self.remoteMethod('sync', {
@ -19,7 +17,7 @@ module.exports = Self => {
}
],
http: {
path: `/sync`,
path: `/:userName/sync`,
verb: 'PATCH'
}
});
@ -35,233 +33,19 @@ module.exports = Self => {
if (user && isSync) return;
let accountConfig;
let mailConfig;
let extraParams;
let hasAccount = false;
if (user) {
accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
user = await $.Account.findById(user.id, {
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'updated'
],
where: {name: userName},
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
extraParams = {
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
};
hasAccount = user.active
&& await $.UserAccount.exists(user.id);
}
if (user) {
let bcryptPassword = $.User.hashPassword(password);
await $.Account.upsertWithWhere({id: user.id},
{bcryptPassword}
);
await $.user.upsert({
id: user.id,
username: userName,
password: bcryptPassword,
email: user.email,
created: user.created,
updated: user.updated
});
}
// SIP
if (hasAccount) {
await Self.rawSql('CALL pbx.sip_setPassword(?, ?)',
[user.id, password]
);
}
// LDAP
let ldapConfig = await $.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn']
});
if (ldapConfig) {
let ldapClient = ldap.createClient({
url: `ldap://${ldapConfig.host}:389`
});
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await ldapClient.bind(ldapConfig.rdn, ldapPassword);
let err;
try {
// Deletes user
let engine = new SyncEngine();
await engine.init($);
try {
let dn = `uid=${userName},${ldapConfig.baseDn}`;
await ldapClient.del(dn);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
// Removes user from groups
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
};
res = await ldapClient.search(ldapConfig.groupDn, opts);
let oldGroups = [];
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldGroups.push(e.object));
res.on('end', resolve);
});
let reqs = [];
for (oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(ldapClient.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (hasAccount) {
// Recreates user
let nameArgs = user.nickname.split(' ');
let sshaPassword = crypto
.createHash('sha1')
.update(password)
.digest('base64');
let dn = `uid=${userName},${ldapConfig.baseDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: user.nickname || userName,
displayName: user.nickname,
givenName: nameArgs[0],
sn: nameArgs[1] || 'Empty',
mail: extraParams.corporateMail,
userPassword: `{SSHA}${sshaPassword}`,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-',
sambaNTPassword: nthash(password)
};
await ldapClient.add(dn, newEntry);
// Adds user to groups
let reqs = [];
for (let role of user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(ldapClient.modify(dn, change));
}
await Promise.all(reqs);
}
await engine.sync(userName, password, true);
await $.UserSync.destroyById(userName);
} catch (e) {
err = e;
}
// FIXME: Cannot disconnect, hangs on undind() call
// await ldapClient.unbind();
await engine.deinit();
if (err) throw err;
}
// Samba
let sambaConfig = await $.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass']
});
if (sambaConfig) {
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let sshClient = new ssh.NodeSSH();
await sshClient.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sshPassword
});
let commands;
if (hasAccount) {
commands = [
`samba-tool user create "${userName}" `
+ `--uid-number=${extraParams.uidNumber} `
+ `--mail-address="${extraParams.corporateMail}" `
+ `--random-password`,
`samba-tool user setexpiry "${userName}" `
+ `--noexpiry`,
`samba-tool user setpassword "${userName}" `
+ `--newpassword="${password}"`,
`mkhomedir_helper "${userName}" 0027`
];
} else {
commands = [
`samba-tool user delete "${userName}"`
];
}
for (let command of commands)
await sshClient.execCommand(command);
await sshClient.dispose();
}
// Mark as synchronized
await $.UserSync.destroyById(userName);
};
};

View File

@ -5,6 +5,9 @@
"LdapConfig": {
"dataSource": "vn"
},
"Mail": {
"dataSource": "vn"
},
"MailAlias": {
"dataSource": "vn"
},
@ -34,8 +37,5 @@
},
"UserSync": {
"dataSource": "vn"
},
"Mail": {
"dataSource": "vn"
}
}

View File

@ -11,7 +11,7 @@
"type": "number",
"id": true
},
"host": {
"server": {
"type": "string",
"required": true
},
@ -23,10 +23,7 @@
"type": "string",
"required": true
},
"baseDn": {
"type": "string"
},
"filter": {
"userDn": {
"type": "string"
},
"groupDn": {

View File

@ -7,7 +7,7 @@
}
},
"properties": {
"role": {
"id": {
"id": true
}
},

View File

@ -18,7 +18,16 @@
"sshUser": {
"type": "string"
},
"sshPass": {
"sshPassword": {
"type": "string"
},
"adUser": {
"type": "string"
},
"adPassword": {
"type": "string"
},
"userDn": {
"type": "string"
}
}

View File

@ -2,4 +2,5 @@
module.exports = Self => {
require('../methods/user-account/sync')(Self);
require('../methods/user-account/sync-by-id')(Self);
require('../methods/user-account/sync-all')(Self);
};

View File

@ -0,0 +1,52 @@
/**
* Base class for user synchronizators.
*
* @property {Array<Model>} $
* @property {Object} accountConfig
* @property {Object} mailConfig
*/
class SyncConnector {
/**
* Initalizes the connector.
*/
async init() {
return true;
}
/**
* Get users to synchronize.
*
* @param {Set} usersToSync Set where users are added
*/
async getUsers(usersToSync) {}
/**
* Synchronizes a user.
*
* @param {Object} info User information
* @param {String} userName The user name
* @param {String} password Thepassword
*/
async sync(info, userName, password) {}
/**
* Synchronizes user groups.
*
* @param {User} user Instace of user
* @param {String} userName The user name
*/
async syncGroups(user, userName) {}
/**
* Synchronizes roles.
*/
async syncRoles() {}
/**
* Deinitalizes the connector.
*/
async deinit() {}
}
SyncConnector.connectors = [];
module.exports = SyncConnector;

View File

@ -0,0 +1,57 @@
const SyncConnector = require('./sync-connector');
class SyncDb extends SyncConnector {
async sync(info, userName, password) {
let {$} = this;
let {user} = info;
if (user && user.active) {
let bcryptPassword = password
? $.User.hashPassword(password)
: user.bcryptPassword;
await $.Account.upsertWithWhere({id: user.id},
{bcryptPassword}
);
let dbUser = {
id: user.id,
username: userName,
email: user.email,
created: user.created,
updated: user.updated
};
if (bcryptPassword)
dbUser.password = bcryptPassword;
if (await $.user.exists(user.id))
await $.user.replaceById(user.id, dbUser);
else
await $.user.create(dbUser);
} else
await $.user.destroyAll({username: userName});
}
async getUsers(usersToSync) {
let accounts = await this.$.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name'],
where: {active: true}
}
}
});
for (let account of accounts) {
let user = account.user();
if (!user) continue;
usersToSync.add(user.name);
}
}
}
SyncConnector.connectors.push(SyncDb);
module.exports = SyncDb;

View File

@ -0,0 +1,127 @@
const SyncConnector = require('./sync-connector');
require('./sync-db');
require('./sync-sip');
require('./sync-ldap');
require('./sync-samba');
module.exports = class SyncEngine {
async init($) {
let accountConfig = await $.AccountConfig.findOne({
fields: ['homedir', 'shell', 'idBase']
});
let mailConfig = await $.MailConfig.findOne({
fields: ['domain']
});
let connectors = [];
for (let ConnectorClass of SyncConnector.connectors) {
let connector = new ConnectorClass();
Object.assign(connector, {
engine: this,
$,
accountConfig,
mailConfig
});
if (!await connector.init()) continue;
connectors.push(connector);
}
Object.assign(this, {
connectors,
$,
accountConfig,
mailConfig
});
}
async deinit() {
for (let connector of this.connectors)
await connector.deinit();
}
async sync(userName, password, syncGroups) {
let {
$,
accountConfig,
mailConfig
} = this;
if (!userName) return;
userName = userName.toLowerCase();
// Skip conflicting users
if (['administrator', 'root'].indexOf(userName) >= 0)
return;
let user = await $.Account.findOne({
where: {name: userName},
fields: [
'id',
'nickname',
'email',
'lang',
'roleFk',
'sync',
'active',
'created',
'bcryptPassword',
'updated'
],
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}
});
let info = {
user,
hasAccount: false
};
if (user) {
let exists = await $.UserAccount.exists(user.id);
Object.assign(info, {
hasAccount: user.active && exists,
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
});
}
let errs = [];
for (let connector of this.connectors) {
try {
await connector.sync(info, userName, password);
if (syncGroups)
await connector.syncGroups(info, userName);
} catch (err) {
errs.push(err);
}
}
if (errs.length) throw errs[0];
}
async syncRoles() {
for (let connector of this.connectors)
await connector.syncRoles();
}
async getUsers() {
let usersToSync = new Set();
for (let connector of this.connectors)
await connector.getUsers(usersToSync);
return usersToSync;
}
};

View File

@ -0,0 +1,293 @@
const SyncConnector = require('./sync-connector');
const nthash = require('smbhash').nthash;
const ldap = require('./ldapjs-extra');
const crypto = require('crypto');
class SyncLdap extends SyncConnector {
async init() {
let ldapConfig = await this.$.LdapConfig.findOne({
fields: [
'server',
'rdn',
'password',
'userDn',
'groupDn'
]
});
if (!ldapConfig) return false;
let client = ldap.createClient({
url: ldapConfig.server
});
await client.bind(ldapConfig.rdn, ldapConfig.password);
Object.assign(this, {
ldapConfig,
client
});
return true;
}
async deinit() {
if (this.client)
await this.client.unbind();
}
async sync(info, userName, password) {
let {
ldapConfig,
client,
accountConfig
} = this;
let {user} = info;
let res = await client.search(ldapConfig.userDn, {
scope: 'sub',
attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})`
});
let oldUser;
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldUser = e.object);
res.on('end', resolve);
});
try {
let dn = `uid=${userName},${ldapConfig.userDn}`;
await client.del(dn);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;
}
if (!info.hasAccount) {
if (oldUser)
console.log(` -> '${userName}' removed from LDAP`);
return;
}
let nickname = user.nickname || userName;
let nameArgs = nickname.trim().split(' ');
let sn = nameArgs.length > 1
? nameArgs.splice(1).join(' ')
: '-';
let dn = `uid=${userName},${ldapConfig.userDn}`;
let newEntry = {
uid: userName,
objectClass: [
'inetOrgPerson',
'posixAccount',
'sambaSamAccount'
],
cn: nickname,
displayName: nickname,
givenName: nameArgs[0],
sn,
mail: info.corporateMail,
preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell,
uidNumber: info.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-'
};
if (password) {
let salt = crypto
.randomBytes(8)
.toString('base64');
let hash = crypto.createHash('sha1');
hash.update(password);
hash.update(salt, 'binary');
let digest = hash.digest('binary');
let ssha = Buffer
.from(digest + salt, 'binary')
.toString('base64');
Object.assign(newEntry, {
userPassword: `{SSHA}${ssha}`,
sambaNTPassword: nthash(password)
});
} else if (oldUser) {
Object.assign(newEntry, {
userPassword: oldUser.userPassword,
sambaNTPassword: oldUser.sambaNTPassword
});
}
for (let prop in newEntry) {
if (newEntry[prop] == null)
delete newEntry[prop];
}
await client.add(dn, newEntry);
}
async syncGroups(info, userName) {
let {
ldapConfig,
client
} = this;
let res = await client.search(ldapConfig.groupDn, {
scope: 'sub',
attributes: ['dn'],
filter: `&(memberUid=${userName})(objectClass=posixGroup)`
});
let oldGroups = [];
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => oldGroups.push(e.object));
res.on('end', resolve);
});
let reqs = [];
for (let oldGroup of oldGroups) {
let change = new ldap.Change({
operation: 'delete',
modification: {memberUid: userName}
});
reqs.push(client.modify(oldGroup.dn, change));
}
await Promise.all(reqs);
if (!info.hasAccount) return;
reqs = [];
for (let role of info.user.roles()) {
let change = new ldap.Change({
operation: 'add',
modification: {memberUid: userName}
});
let roleName = role.inherits().name;
let dn = `cn=${roleName},${ldapConfig.groupDn}`;
reqs.push(client.modify(dn, change));
}
await Promise.all(reqs);
}
async getUsers(usersToSync) {
let {
ldapConfig,
client
} = this;
let res = await client.search(ldapConfig.userDn, {
scope: 'sub',
attributes: ['uid'],
filter: `uid=*`
});
await new Promise((resolve, reject) => {
res.on('error', reject);
res.on('searchEntry', e => usersToSync.add(e.object.uid));
res.on('end', resolve);
});
}
async syncRoles() {
let {
$,
ldapConfig,
client,
accountConfig
} = this;
// Delete roles
let opts = {
scope: 'sub',
attributes: ['dn'],
filter: 'objectClass=posixGroup'
};
let res = await client.search(ldapConfig.groupDn, opts);
let reqs = [];
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${ldapConfig.groupDn}' does not exist`);
reject(err);
});
res.on('searchEntry', e => {
reqs.push(client.del(e.object.dn));
});
res.on('end', resolve);
});
await Promise.all(reqs);
// Recreate roles
let roles = await $.Role.find({
fields: ['id', 'name']
});
let roleRoles = await $.RoleRole.find({
fields: ['role', 'inheritsFrom']
});
let roleMap = toMap(roleRoles, e => {
return {key: e.inheritsFrom, val: e.role};
});
let accounts = await $.UserAccount.find({
fields: ['id'],
include: {
relation: 'user',
scope: {
fields: ['name', 'roleFk'],
where: {active: true}
}
}
});
let accountMap = toMap(accounts, e => {
let user = e.user();
if (!user) return;
return {key: user.roleFk, val: user.name};
});
console.log;
reqs = [];
for (let role of roles) {
let newEntry = {
objectClass: ['top', 'posixGroup'],
cn: role.name,
gidNumber: accountConfig.idBase + role.id
};
let memberUid = [];
for (let subrole of roleMap.get(role.id) || [])
memberUid = memberUid.concat(accountMap.get(subrole) || []);
if (memberUid.length) {
memberUid.sort((a, b) => a.localeCompare(b));
newEntry.memberUid = memberUid;
}
let dn = `cn=${role.name},${ldapConfig.groupDn}`;
reqs.push(client.add(dn, newEntry));
}
await Promise.all(reqs);
}
}
SyncConnector.connectors.push(SyncLdap);
module.exports = SyncLdap;
function toMap(array, fn) {
let map = new Map();
for (let item of array) {
let keyVal = fn(item);
if (!keyVal) continue;
let key = keyVal.key;
if (!map.has(key)) map.set(key, []);
map.get(key).push(keyVal.val);
}
return map;
}

View File

@ -0,0 +1,125 @@
const SyncConnector = require('./sync-connector');
const ssh = require('node-ssh');
const ldap = require('./ldapjs-extra');
class SyncSamba extends SyncConnector {
async init() {
let sambaConfig = await this.$.SambaConfig.findOne({
fields: [
'host',
'sshUser',
'sshPassword',
'adUser',
'adPassword',
'userDn'
]
});
if (!sambaConfig) return false;
let client = new ssh.NodeSSH();
await client.connect({
host: sambaConfig.host,
username: sambaConfig.sshUser,
password: sambaConfig.sshPassword
});
let adClient = ldap.createClient({
url: `ldaps://${sambaConfig.host}:636`,
tlsOptions: {rejectUnauthorized: false}
});
Object.assign(this, {
sambaConfig,
client,
adClient
});
return true;
}
async deinit() {
if (!this.client) return;
await this.client.dispose();
await this.adClient.unbind();
}
async sync(info, userName, password) {
let {client} = this;
if (info.hasAccount) {
try {
await client.exec('samba-tool user create', [
userName,
'--uid-number', `${info.uidNumber}`,
'--mail-address', info.corporateMail,
'--random-password'
]);
await client.exec('samba-tool user setexpiry', [
userName,
'--noexpiry'
]);
await client.exec('mkhomedir_helper', [
userName,
'0027'
]);
} catch (e) {}
await client.exec('samba-tool user enable', [
userName
]);
if (password) {
await client.exec('samba-tool user setpassword', [
userName,
'--newpassword', password
]);
}
} else {
try {
await client.exec('samba-tool user disable', [
userName
]);
console.log(` -> '${userName}' disabled on Samba`);
} catch (e) {}
}
}
/**
* Gets enabled users from Samba.
*
* Summary of userAccountControl flags:
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
*
* @param {Set} usersToSync
*/
async getUsers(usersToSync) {
let {
sambaConfig,
adClient
} = this;
await adClient.bind(sambaConfig.adUser, sambaConfig.adPassword);
let opts = {
scope: 'sub',
attributes: ['sAMAccountName'],
filter: '(&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))'
};
let res = await adClient.search(sambaConfig.userDn, opts);
await new Promise((resolve, reject) => {
res.on('error', err => {
if (err.name === 'NoSuchObjectError')
err = new Error(`Object '${sambaConfig.userDn}' does not exist`);
reject(err);
});
res.on('searchEntry', e => {
usersToSync.add(e.object.sAMAccountName);
});
res.on('end', resolve);
});
}
}
SyncConnector.connectors.push(SyncSamba);
module.exports = SyncSamba;

View File

@ -0,0 +1,15 @@
const SyncConnector = require('./sync-connector');
class SyncSip extends SyncConnector {
async sync(info, userName, password) {
if (!info.hasAccount || !password) return;
await this.$.Account.rawSql('CALL pbx.sip_setPassword(?, ?)',
[info.user.id, password]
);
}
}
SyncConnector.connectors.push(SyncSip);
module.exports = SyncSip;

View File

@ -15,3 +15,6 @@ import './basic-data';
import './mail-forwarding';
import './aliases';
import './roles';
import './ldap';
import './samba';
import './posix';

View File

@ -0,0 +1,95 @@
<vn-watcher
vn-id="watcher"
url="LdapConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-check
label="Enable synchronization"
ng-model="watcher.hasData">
</vn-check>
</vn-vertical>
<vn-vertical
ng-if="watcher.hasData"
class="vn-mt-md">
<vn-textfield
label="Server"
ng-model="$ctrl.config.server"
rule="LdapConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="RDN"
ng-model="$ctrl.config.rdn"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.config.password"
type="password"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="User DN"
ng-model="$ctrl.config.userDn"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Group DN"
ng-model="$ctrl.config.groupDn"
rule="LdapConfig">
</vn-textfield>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
<vn-button
label="Synchronize now"
ng-if="watcher.hasData"
ng-click="$ctrl.onSynchronizeAll()">
</vn-button>
<vn-button
label="Synchronize user"
ng-if="watcher.hasData"
ng-click="syncUser.show()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onUserSync()"
on-close="$ctrl.onPassClose()">
<tpl-body>
<vn-textfield
label="Username"
ng-model="$ctrl.syncUser">
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
info="If password is not specified, just user attributes are synchronized">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,30 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section {
onSynchronizeAll() {
this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
this.$http.patch(`UserAccounts/syncAll`)
.then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
}
onUserSync() {
if (!this.syncUser)
throw new UserError('Please enter the username');
let params = {password: this.syncPassword};
return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
}
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
}
ngModule.component('vnAccountLdap', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,16 @@
Enable synchronization: Habilitar sincronización
Server: Servidor
RDN: RDN
User DN: DN usuarios
Filter: Filtro
Group DN: DN grupos
Synchronize now: Sincronizar ahora
Synchronize user: Sincronizar usuario
If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
Synchronizing in the background: Sincronizando en segundo plano
Users synchronized!: ¡Usuarios sincronizados!
Username: Nombre de usuario
Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario
User synchronized!: ¡Usuario sincronizado!

View File

@ -0,0 +1,69 @@
<vn-watcher
vn-id="watcher"
url="AccountConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-textfield
label="Homedir"
ng-model="$ctrl.config.homedir"
rule="AccountConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="Shell"
ng-model="$ctrl.config.shell"
rule="AccountConfig">
</vn-textfield>
<vn-input-number
label="Id base"
ng-model="$ctrl.config.idBase"
rule="AccountConfig">
</vn-input-number>
<vn-horizontal>
<vn-input-number
label="Min"
ng-model="$ctrl.config.min"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Max"
ng-model="$ctrl.config.max"
rule="AccountConfig">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
label="Warn"
ng-model="$ctrl.config.warn"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Inact"
ng-model="$ctrl.config.inact"
rule="AccountConfig">
</vn-input-number>
</vn-horizontal>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
ngModule.component('vnAccountPosix', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,16 @@
Host: Host
RDN: RDN
Base DN: DN base
Password should be base64 encoded: La contraseña debe estar codificada en base64
Filter: Filtro
Group DN: DN grupos
Synchronize now: Sincronizar ahora
Synchronize user: Sincronizar usuario
If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
Synchronizing in the background: Sincronizando en segundo plano
LDAP users synchronized: Usuarios LDAP sincronizados
Username: Nombre de usuario
Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario
User synchronized: Usuario sincronizado

View File

@ -9,6 +9,9 @@
{"state": "account.index", "icon": "face"},
{"state": "account.role", "icon": "group"},
{"state": "account.alias", "icon": "email"},
{"state": "account.posix", "icon": "accessibility"},
{"state": "account.ldap", "icon": "account_tree"},
{"state": "account.samba", "icon": "desktop_windows"},
{"state": "account.acl", "icon": "check"},
{"state": "account.connections", "icon": "share"}
],
@ -174,6 +177,24 @@
"state": "account.alias.card.users",
"component": "vn-alias-users",
"description": "Users"
}, {
"url": "/posix",
"state": "account.posix",
"component": "vn-account-posix",
"description": "Posix",
"acl": ["developer"]
}, {
"url": "/ldap",
"state": "account.ldap",
"component": "vn-account-ldap",
"description": "LDAP",
"acl": ["developer"]
}, {
"url": "/samba",
"state": "account.samba",
"component": "vn-account-samba",
"description": "Samba",
"acl": ["developer"]
}, {
"url": "/acl?q",
"state": "account.acl",

View File

@ -0,0 +1,70 @@
<vn-watcher
vn-id="watcher"
url="SambaConfigs"
data="$ctrl.config"
id-value="1"
form="form">
</vn-watcher>
<form
name="form"
ng-submit="watcher.submit()"
class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-vertical>
<vn-check
label="Enable synchronization"
ng-model="watcher.hasData">
</vn-check>
</vn-vertical>
<vn-vertical
ng-if="watcher.hasData"
class="vn-mt-md">
<vn-textfield
label="Host"
ng-model="$ctrl.config.host"
rule="SambaConfig"
vn-focus>
</vn-textfield>
<vn-textfield
label="SSH user"
ng-model="$ctrl.config.sshUser"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="SSH password"
ng-model="$ctrl.config.sshPassword"
type="password"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD user"
ng-model="$ctrl.config.adUser"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD password"
ng-model="$ctrl.config.adPassword"
type="password"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="User DN"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
</vn-vertical>
</vn-card>
<vn-button-bar>
<vn-button
label="Undo changes"
ng-if="watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
<vn-submit
icon="save"
vn-tooltip="Save"
class="round"
fixed-bottom-right>
</vn-submit>
</form>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {}
ngModule.component('vnAccountSamba', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,7 @@
Enable synchronization: Habilitar sincronización
Host: Host
SSH user: Usuario SSH
SSH password: Contraseña SSH
AD user: Usuario AD
AD password: Contraseña AD
User DN: DN usuarios

View File

@ -4,7 +4,7 @@ module.exports = function(Self) {
Self.remoteMethodCtx('createAddress', {
description: 'Creates client address updating default address',
accepts: [{
arg: 'id',
arg: 'clientFk',
type: 'number',
description: 'The client id',
http: {source: 'path'}
@ -37,19 +37,19 @@ module.exports = function(Self) {
type: 'string'
},
{
arg: 'provinceId',
arg: 'provinceFk',
type: 'number'
},
{
arg: 'agencyModeId',
arg: 'agencyModeFk',
type: 'number'
},
{
arg: 'incotermsId',
arg: 'incotermsFk',
type: 'string'
},
{
arg: 'customsAgentId',
arg: 'customsAgentFk',
type: 'number'
},
{
@ -66,44 +66,33 @@ module.exports = function(Self) {
},
http: {
verb: 'post',
path: '/:id/createAddress'
path: '/:clientFk/createAddress'
}
});
Self.createAddress = async(ctx, clientId) => {
Self.createAddress = async(ctx, clientFk) => {
const models = Self.app.models;
const args = ctx.args;
const tx = await models.Address.beginTransaction({});
try {
const options = {transaction: tx};
const province = await models.Province.findById(args.provinceId, {
const province = await models.Province.findById(args.provinceFk, {
include: {
relation: 'country'
}
}, options);
const isUeeMember = province.country().isUeeMember;
if (!isUeeMember && !args.incotermsId)
if (!isUeeMember && !args.incotermsFk)
throw new UserError(`Incoterms is required for a non UEE member`);
if (!isUeeMember && !args.customsAgentId)
if (!isUeeMember && !args.customsAgentFk)
throw new UserError(`Customs agent is required for a non UEE member`);
const newAddress = await models.Address.create({
clientFk: clientId,
nickname: args.nickname,
incotermsFk: args.incotermsId,
customsAgentFk: args.customsAgentId,
city: args.city,
street: args.street,
phone: args.phone,
postalCode: args.postalCode,
provinceFk: args.provinceId,
agencyModeFk: args.agencyModeId,
isActive: args.isActive
}, options);
const client = await Self.findById(clientId, null, options);
delete args.ctx; // Remove unwanted properties
const newAddress = await models.Address.create(args, options);
const client = await Self.findById(clientFk, null, options);
if (args.isDefaultAddress) {
await client.updateAttributes({

View File

@ -1,25 +1,25 @@
const app = require('vn-loopback/server/server');
describe('Address createAddress', () => {
const clientId = 101;
const provinceId = 5;
const incotermsId = 'FAS';
const clientFk = 101;
const provinceFk = 5;
const incotermsFk = 'FAS';
const customAgentOneId = 1;
it('should throw a non uee member error if no incoterms is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
provinceFk: provinceFk,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
customsAgentId: customAgentOneId
customsAgentFk: customAgentOneId
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
await app.models.Client.createAddress(ctx, clientFk);
} catch (e) {
err = e;
}
@ -32,16 +32,16 @@ describe('Address createAddress', () => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
provinceFk: provinceFk,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId
incotermsFk: incotermsFk
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
await app.models.Client.createAddress(ctx, clientFk);
} catch (e) {
err = e;
}
@ -51,7 +51,7 @@ describe('Address createAddress', () => {
});
it('should verify that client defaultAddressFk is untainted', async() => {
const client = await app.models.Client.findById(clientId);
const client = await app.models.Client.findById(clientFk);
expect(client.defaultAddressFk).toEqual(1);
});
@ -59,18 +59,23 @@ describe('Address createAddress', () => {
it('should create a new address and set as a client default address', async() => {
const ctx = {
args: {
provinceId: 1,
clientFk: 101,
provinceFk: 1,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId,
customsAgentId: customAgentOneId,
phone: 678678678,
mobile: 678678678,
postalCode: 46680,
agencyModeFk: 1,
incotermsFk: incotermsFk,
customsAgentFk: customAgentOneId,
isDefaultAddress: true
}
};
const address = await app.models.Client.createAddress(ctx, clientId);
const client = await app.models.Client.findById(clientId);
const address = await app.models.Client.createAddress(ctx, clientFk);
const client = await app.models.Client.findById(clientFk);
expect(client.defaultAddressFk).toEqual(address.id);
@ -78,4 +83,31 @@ describe('Address createAddress', () => {
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
});
it('should create a new address and set all properties', async() => {
const ctx = {
args: {
clientFk: 101,
provinceFk: 1,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
phone: '678678678',
mobile: '678678678',
postalCode: '46680',
agencyModeFk: 1,
incotermsFk: incotermsFk,
customsAgentFk: customAgentOneId,
isDefaultAddress: true
}
};
address = await app.models.Client.createAddress(ctx, clientFk);
expect(address).toEqual(jasmine.objectContaining(ctx.args));
// restores
const client = await app.models.Client.findById(clientFk);
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
});
});

View File

@ -2,9 +2,11 @@ const app = require('vn-loopback/server/server');
describe('Client updateFiscalData', () => {
const clientId = 101;
const employeeId = 1;
const salesAssistantId = 21;
const administrativeId = 5;
afterAll(async done => {
const clientId = 101;
const ctx = {req: {accessToken: {userId: 5}}};
const ctx = {req: {accessToken: {userId: administrativeId}}};
ctx.args = {postcode: 46460};
await app.models.Client.updateFiscalData(ctx, clientId);
@ -12,8 +14,8 @@ describe('Client updateFiscalData', () => {
done();
});
it('should return an error if the user is not administrative and the isTaxDataChecked value is true', async() => {
const ctx = {req: {accessToken: {userId: 1}}};
it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => {
const ctx = {req: {accessToken: {userId: employeeId}}};
ctx.args = {};
let error;
@ -22,11 +24,30 @@ describe('Client updateFiscalData', () => {
error = e;
});
expect(error.message).toBeDefined();
expect(error.message).toEqual(`You can't make changes on a client with verified data`);
});
it('should return an error if the salesAssistant did not fill the sage data before checking verified data', async() => {
const client = await app.models.Client.findById(clientId);
await client.updateAttribute('isTaxDataChecked', false);
const ctx = {req: {accessToken: {userId: salesAssistantId}}};
ctx.args = {isTaxDataChecked: true};
let error;
await app.models.Client.updateFiscalData(ctx, clientId)
.catch(e => {
error = e;
});
expect(error.message).toEqual('You need to fill sage information before you check verified data');
// Restores
await client.updateAttribute('isTaxDataChecked', true);
});
it('should update the client fiscal data and return the count if changes made', async() => {
const ctx = {req: {accessToken: {userId: 5}}};
const ctx = {req: {accessToken: {userId: administrativeId}}};
ctx.args = {postcode: 46680};
const client = await app.models.Client.findById(clientId);

View File

@ -45,15 +45,15 @@ module.exports = Self => {
},
{
arg: 'sageTaxTypeFk',
type: 'number'
type: 'any'
},
{
arg: 'sageTransactionTypeFk',
type: 'number'
type: 'any'
},
{
arg: 'transferorFk',
type: 'number'
type: 'any'
},
{
arg: 'hasToInvoiceByAddress',
@ -118,6 +118,15 @@ module.exports = Self => {
if (!isSalesAssistant && client.isTaxDataChecked)
throw new UserError(`You can't make changes on a client with verified data`);
// Sage data validation
const taxDataChecked = args.isTaxDataChecked;
const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk;
const sageTransactionChecked = client.sageTransactionTypeFk || args.sageTransactionTypeFk;
const hasSageData = sageTaxChecked && sageTransactionChecked;
if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`);
if (args.despiteOfClient) {
const logRecord = {
originFk: clientId,

View File

@ -1,7 +1,9 @@
let request = require('request-promise-native');
let UserError = require('vn-loopback/util/user-error');
let getFinalState = require('vn-loopback/util/hook').getFinalState;
let isMultiple = require('vn-loopback/util/hook').isMultiple;
const request = require('request-promise-native');
const UserError = require('vn-loopback/util/user-error');
const getFinalState = require('vn-loopback/util/hook').getFinalState;
const isMultiple = require('vn-loopback/util/hook').isMultiple;
const validateTin = require('vn-loopback/util/validateTin');
const validateIban = require('vn-loopback/util/validateIban');
const LoopBackContext = require('loopback-context');
module.exports = Self => {
@ -63,7 +65,7 @@ module.exports = Self => {
Self.validateAsync('iban', ibanNeedsValidation, {
message: 'The IBAN does not have the correct format'
});
let validateIban = require('../validations/validateIban');
async function ibanNeedsValidation(err, done) {
let filter = {
fields: ['code'],
@ -83,7 +85,6 @@ module.exports = Self => {
message: 'Invalid TIN'
});
let validateTin = require('../validations/validateTin');
async function tinIsValid(err, done) {
if (!this.isTaxDataChecked)
return done();
@ -187,7 +188,7 @@ module.exports = Self => {
// Validate socialName format
const hasChanges = orgData && changes;
const socialName = changes.socialName || orgData.socialName;
const socialName = changes && changes.socialName || orgData && orgData.socialName;
const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);
const socialNameChanged = hasChanges

View File

@ -15,6 +15,10 @@
"description": {
"type": "String",
"required": true
},
"code": {
"type": "String",
"required": true
}
},
"acls": [

View File

@ -88,7 +88,7 @@
</vn-datalist>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.address.provinceId"
ng-model="$ctrl.address.provinceFk"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
@ -100,7 +100,7 @@
<vn-horizontal>
<vn-autocomplete
vn-one
ng-model="$ctrl.address.agencyModeId"
ng-model="$ctrl.address.agencyModeFk"
url="AgencyModes/isActive"
show-field="name"
value-field="id"
@ -121,14 +121,14 @@
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.address.incotermsId"
ng-model="$ctrl.address.incotermsFk"
data="incoterms"
show-field="name"
value-field="code"
label="Incoterms">
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.address.customsAgentId"
ng-model="$ctrl.address.customsAgentFk"
data="customsAgents"
show-field="fiscalName"
value-field="id"

View File

@ -29,7 +29,7 @@ export default class Controller extends Section {
onCustomAgentAccept() {
return this.$http.post(`CustomsAgents`, this.newCustomsAgent)
.then(res => this.address.customsAgentId = res.data.id);
.then(res => this.address.customsAgentFk = res.data.id);
}
get town() {
@ -45,8 +45,8 @@ export default class Controller extends Section {
const province = selection.province;
const postcodes = selection.postcodes;
if (!this.address.provinceI)
this.address.provinceId = province.id;
if (!this.address.provinceFk)
this.address.provinceFk = province.id;
if (postcodes.length === 1)
this.address.postalCode = postcodes[0].code;
@ -68,8 +68,8 @@ export default class Controller extends Section {
if (!this.address.city)
this.address.city = town.name;
if (!this.address.provinceId)
this.address.provinceId = province.id;
if (!this.address.provinceFk)
this.address.provinceFk = province.id;
}
onResponse(response) {

View File

@ -54,7 +54,7 @@ describe('Client', () => {
});
describe('town() setter', () => {
it(`should set provinceId property`, () => {
it(`should set provinceFk property`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
@ -69,10 +69,10 @@ describe('Client', () => {
postcodes: []
};
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
});
it(`should set provinceId property and fill the postalCode if there's just one`, () => {
it(`should set provinceFk property and fill the postalCode if there's just one`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
@ -87,7 +87,7 @@ describe('Client', () => {
postcodes: [{code: '46001'}]
};
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
expect(controller.address.postalCode).toEqual('46001');
});
});
@ -112,7 +112,7 @@ describe('Client', () => {
};
expect(controller.address.city).toEqual('New York');
expect(controller.address.provinceId).toEqual(1);
expect(controller.address.provinceFk).toEqual(1);
});
});
@ -123,7 +123,7 @@ describe('Client', () => {
controller.onCustomAgentAccept();
$httpBackend.flush();
expect(controller.address.customsAgentId).toEqual(1);
expect(controller.address.customsAgentFk).toEqual(1);
});
});
});

View File

@ -52,7 +52,7 @@ export default class Controller extends Section {
if (!this.address.provinceFk)
this.address.provinceFk = province.id;
if (postcodes.length === 1)
if (!this.address.postalCode && postcodes.length === 1)
this.address.postalCode = postcodes[0].code;
}

View File

@ -39,7 +39,8 @@
label="Social name"
ng-model="$ctrl.client.socialName"
rule
info="You can use letters and spaces">
info="You can use letters and spaces"
required="true">
</vn-textfield>
<vn-textfield
vn-one
@ -63,6 +64,7 @@
show-field="vat"
value-field="id"
label="Sage tax type"
vn-acl="salesAssistant"
rule>
</vn-autocomplete>
<vn-autocomplete vn-one
@ -71,6 +73,7 @@
show-field="transaction"
value-field="id"
label="Sage transaction type"
vn-acl="salesAssistant"
rule>
</vn-autocomplete>
<vn-autocomplete vn-one
@ -82,6 +85,7 @@
value-field="id"
label="Previous client"
info="In case of a company succession, specify the grantor company"
vn-acl="salesAssistant"
rule>
</vn-autocomplete>
</vn-horizontal>

View File

@ -128,7 +128,7 @@ export default class Controller extends Section {
if (!this.client.countryFk)
this.client.countryFk = country.id;
if (postcodes.length === 1)
if (!this.client.postcode && postcodes.length === 1)
this.client.postcode = postcodes[0].code;
}

View File

@ -7,7 +7,7 @@ Has to invoice: Factura
Notify by email: Notificar vía e-mail
Country: País
Street: Domicilio fiscal
City: Municipio
City: Ciudad
Postcode: Código postal
Province: Provincia
Address: Consignatario

View File

@ -190,8 +190,6 @@ module.exports = Self => {
item.prices.push(price);
else
item.prices = [price];
item.available = price.grouping;
}
});
});

View File

@ -7,7 +7,7 @@ describe('Supplier getSummary()', () => {
expect(supplier.id).toEqual(1);
expect(supplier.name).toEqual('Plants SL');
expect(supplier.nif).toEqual('06089160W');
expect(supplier.account).toEqual(4000000001);
expect(supplier.account).toEqual('4100000001');
expect(supplier.payDay).toEqual(15);
});

View File

@ -0,0 +1,88 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('Supplier updateFiscalData', () => {
const supplierId = 1;
const administrativeId = 5;
const employeeId = 1;
const defaultData = {
name: 'Plants SL',
nif: '06089160W',
account: '4100000001',
sageTaxTypeFk: 4,
sageWithholdingFk: 1,
sageTransactionTypeFk: 1,
postCode: '15214',
city: 'PONTEVEDRA',
provinceFk: 1,
countryFk: 1,
};
it('should return an error if the user is not administrative', async() => {
const ctx = {req: {accessToken: {userId: employeeId}}};
ctx.args = {};
let error;
await app.models.Supplier.updateFiscalData(ctx, supplierId)
.catch(e => {
error = e;
});
expect(error.message).toBeDefined();
});
it('should check that the supplier fiscal data is untainted', async() => {
const supplier = await app.models.Supplier.findById(supplierId);
expect(supplier.name).toEqual(defaultData.name);
expect(supplier.nif).toEqual(defaultData.nif);
expect(supplier.account).toEqual(defaultData.account);
expect(supplier.sageTaxTypeFk).toEqual(defaultData.sageTaxTypeFk);
expect(supplier.sageWithholdingFk).toEqual(defaultData.sageWithholdingFk);
expect(supplier.sageTransactionTypeFk).toEqual(defaultData.sageTransactionTypeFk);
expect(supplier.postCode).toEqual(defaultData.postCode);
expect(supplier.city).toEqual(defaultData.city);
expect(supplier.provinceFk).toEqual(defaultData.provinceFk);
expect(supplier.countryFk).toEqual(defaultData.countryFk);
});
it('should update the supplier fiscal data and return the count if changes made', async() => {
const activeCtx = {
accessToken: {userId: administrativeId},
};
const ctx = {req: activeCtx};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
ctx.args = {
name: 'Weapon Dealer',
nif: 'A68446004',
account: '4000000005',
sageTaxTypeFk: 5,
sageWithholdingFk: 2,
sageTransactionTypeFk: 2,
postCode: '46460',
city: 'VALENCIA',
provinceFk: 2,
countryFk: 1,
};
const result = await app.models.Supplier.updateFiscalData(ctx, supplierId);
expect(result.name).toEqual('Weapon Dealer');
expect(result.nif).toEqual('A68446004');
expect(result.account).toEqual('4000000005');
expect(result.sageTaxTypeFk).toEqual(5);
expect(result.sageWithholdingFk).toEqual(2);
expect(result.sageTransactionTypeFk).toEqual(2);
expect(result.postCode).toEqual('46460');
expect(result.city).toEqual('VALENCIA');
expect(result.provinceFk).toEqual(2);
expect(result.countryFk).toEqual(1);
// Restores
ctx.args = defaultData;
await app.models.Supplier.updateFiscalData(ctx, supplierId);
});
});

View File

@ -0,0 +1,78 @@
module.exports = Self => {
Self.remoteMethod('updateFiscalData', {
description: 'Updates fiscal data of a supplier',
accessType: 'WRITE',
accepts: [{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'id',
type: 'Number',
description: 'The supplier id',
http: {source: 'path'}
},
{
arg: 'name',
type: 'string'
},
{
arg: 'nif',
type: 'string'
},
{
arg: 'account',
type: 'string'
},
{
arg: 'sageTaxTypeFk',
type: 'number'
},
{
arg: 'sageWithholdingFk',
type: 'number'
},
{
arg: 'sageTransactionTypeFk',
type: 'number'
},
{
arg: 'postCode',
type: 'string'
},
{
arg: 'city',
type: 'string'
},
{
arg: 'provinceFk',
type: 'number'
},
{
arg: 'countryFk',
type: 'number'
}],
returns: {
arg: 'res',
type: 'string',
root: true
},
http: {
path: `/:id/updateFiscalData`,
verb: 'PATCH'
}
});
Self.updateFiscalData = async(ctx, supplierId) => {
const models = Self.app.models;
const args = ctx.args;
const supplier = await models.Supplier.findById(supplierId);
// Remove unwanted properties
delete args.ctx;
delete args.id;
return supplier.updateAttributes(args);
};
};

View File

@ -7,5 +7,8 @@
},
"SupplierLog": {
"dataSource": "vn"
},
"SupplierContact": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,49 @@
{
"name": "SupplierContact",
"base": "VnModel",
"options": {
"mysql": {
"table": "supplierContact"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"supplierFk": {
"type": "String"
},
"phone": {
"type": "String"
},
"mobile": {
"type": "String"
},
"email": {
"type": "String"
},
"observation": {
"type": "String"
},
"name": {
"type": "String"
}
},
"relations": {
"supplier": {
"type": "belongsTo",
"model": "Supplier",
"foreignKey": "supplierFk"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -1,4 +1,85 @@
const UserError = require('vn-loopback/util/user-error');
const validateTin = require('vn-loopback/util/validateTin');
module.exports = Self => {
require('../methods/supplier/filter')(Self);
require('../methods/supplier/getSummary')(Self);
require('../methods/supplier/updateFiscalData')(Self);
Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty'
});
Self.validatesUniquenessOf('name', {
message: 'The supplier name must be unique'
});
Self.validatesPresenceOf('city', {
message: 'City cannot be empty'
});
Self.validatesPresenceOf('nif', {
message: 'The nif cannot be empty'
});
Self.validatesUniquenessOf('nif', {
message: 'TIN must be unique'
});
Self.validateAsync('nif', tinIsValid, {
message: 'Invalid TIN'
});
Self.validatesLengthOf('postCode', {
allowNull: true,
allowBlank: true,
min: 3, max: 10
});
Self.validateAsync('postCode', hasValidPostcode, {
message: `The postcode doesn't exist. Please enter a correct one`
});
async function hasValidPostcode(err, done) {
if (!this.postcode)
return done();
const models = Self.app.models;
const postcode = await models.Postcode.findById(this.postcode);
if (!postcode) err();
done();
}
async function tinIsValid(err, done) {
const filter = {
fields: ['code'],
where: {id: this.countryFk}
};
const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null;
if (!this.nif || !validateTin(this.nif, code))
err();
done();
}
function isAlpha(value) {
const regexp = new RegExp(/^[ñça-zA-Z0-9\s]*$/i);
return regexp.test(value);
}
Self.observe('before save', async function(ctx) {
let changes = ctx.data || ctx.instance;
let orgData = ctx.currentInstance;
const socialName = changes.name || orgData.name;
const hasChanges = orgData && changes;
const socialNameChanged = hasChanges
&& orgData.socialName != socialName;
if ((socialNameChanged) && !isAlpha(socialName))
throw new UserError('The socialName has an invalid format');
});
};

View File

@ -19,7 +19,7 @@
"type": "String"
},
"account": {
"type": "Number"
"type": "String"
},
"countryFk": {
"type": "Number"
@ -64,7 +64,7 @@
"type": "Number"
},
"postCode": {
"type": "Number"
"type": "String"
},
"payMethodFk": {
"type": "Number"
@ -77,6 +77,24 @@
},
"nickname": {
"type": "String"
},
"sageTaxTypeFk": {
"type": "number",
"mysql": {
"columnName": "taxTypeSageFk"
}
},
"sageTransactionTypeFk": {
"type": "number",
"mysql": {
"columnName": "transactionTypeSageFk"
}
},
"sageWithholdingFk": {
"type": "number",
"mysql": {
"columnName": "withholdingSageFk"
}
}
},
"relations": {

View File

@ -0,0 +1,74 @@
<vn-crud-model
vn-id="model"
url="SupplierContacts"
link="{supplierFk: $ctrl.$params.id}"
data="contacts"
auto-load="true">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="contacts"
form="form">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<div ng-repeat="contact in contacts" class="contact">
<vn-vertical>
<vn-horizontal>
<vn-textfield
vn-one
label="Name"
ng-model="contact.name"
rule="SupplierContact">
</vn-textfield>
<vn-textfield
vn-one
label="Phone"
ng-model="contact.phone"
rule="SupplierContact">
</vn-textfield>
<vn-textfield
vn-one
label="Mobile"
ng-model="contact.mobile"
rule="SupplierContact">
</vn-textfield>
<vn-textfield
vn-one
label="Email"
ng-model="contact.email"
rule="SupplierContact">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Notes"
ng-model="contact.observation"
rule="SupplierContact"
title="{{contact.observation}}">
</vn-textfield>
<vn-none>
<vn-icon-button
vn-tooltip="Remove contact"
icon="delete"
tabindex="-1"
ng-click="model.remove($index)">
</vn-icon-button>
</vn-none>
</vn-horizontal>
</vn-vertical>
</div>
<vn-one>
<vn-icon-button
vn-bind="+"
vn-tooltip="Add contact"
icon="add_circle"
ng-click="$ctrl.add()">
</vn-icon-button>
</vn-one>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,27 @@
import ngModule from '../module';
import './style.scss';
import Section from 'salix/components/section';
class Controller extends Section {
add() {
this.$.model.insert({
supplierFk: this.supplier.id
});
}
onSubmit() {
this.$.watcher.check();
this.$.model.save().then(() => {
this.$.watcher.notifySaved();
this.$.watcher.updateOriginalData();
});
}
}
ngModule.vnComponent('vnSupplierContact', {
template: require('./index.html'),
controller: Controller,
bindings: {
supplier: '<'
}
});

View File

@ -0,0 +1,10 @@
@import "variables";
.contact {
max-width: $width-lg;
margin-bottom: 10px;
padding-right: 10px;
padding-left: 10px;
border: 1px solid $color-marginal;
}

View File

@ -0,0 +1,165 @@
<mg-ajax path="Suppliers/{{patch.params.id}}/updateFiscalData" options="vnPatch"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.supplier"
id-field="id"
form="form"
save="patch">
</vn-watcher>
<vn-crud-model
auto-load="true"
url="Provinces/location"
data="provincesLocation"
order="name">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Countries"
data="countries"
order="country">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTaxTypes"
data="sageTaxTypes"
order="vat">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTransactionTypes"
data="sageTransactionTypes"
order="transaction">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageWithholdings"
data="sageWithholdings"
order="withholding">
</vn-crud-model>
<form name="form" vn-http-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
vn-two
vn-focus
label="Social name"
ng-model="$ctrl.supplier.name"
info="You can use letters and spaces"
required="true"
rule>
</vn-textfield>
<vn-textfield
vn-one
label="Tax number"
ng-model="$ctrl.supplier.nif"
required="true"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Account"
ng-model="$ctrl.supplier.account"
rule>
</vn-textfield>
<vn-autocomplete vn-one
ng-model="$ctrl.supplier.sageTaxTypeFk"
data="sageTaxTypes"
show-field="vat"
value-field="id"
label="Sage tax type"
rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.supplier.sageWithholdingFk"
data="sageWithholdings"
show-field="withholding"
value-field="id"
label="Sage withholding"
rule>
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.supplier.sageTransactionTypeFk"
data="sageTransactionTypes"
show-field="transaction"
value-field="id"
label="Sage transaction type"
rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Postcode"
ng-model="$ctrl.supplier.postCode"
selection="$ctrl.postcode"
url="Postcodes/location"
fields="['code','townFk']"
order="code, townFk"
value-field="code"
show-field="code"
rule>
<tpl-item>
{{code}} - {{town.name}} ({{town.province.name}},
{{town.province.country.country}})
</tpl-item>
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New postcode"
ng-click="postcode.open()"
vn-acl="deliveryBoss"
vn-acl-action="remove">
</vn-icon-button>
</append>
</vn-datalist>
<vn-datalist vn-id="town" vn-one
label="City"
ng-model="$ctrl.supplier.city"
selection="$ctrl.town"
url="Towns/location"
fields="['id', 'name', 'provinceFk']"
show-field="name"
value-field="name"
required="true"
rule>
<tpl-item>
{{name}}, {{province.name}}
({{province.country.country}})
</tpl-item>
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.supplier.provinceFk"
selection="$ctrl.province"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
value-field="id"
rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-id="country" vn-one
ng-model="$ctrl.supplier.countryFk"
data="countries"
show-field="country"
value-field="id"
label="Country"
rule>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
<vn-button label="Undo changes" ng-if="$ctrl.$.form.$dirty" ng-click="watcher.loadOriginalData()"></vn-button>
</vn-button-bar>
</form>
<!-- New postcode dialog -->
<vn-geo-postcode
vn-id="postcode"
on-response="$ctrl.onResponse($response)">
</vn-geo-postcode>

View File

@ -0,0 +1,84 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
get province() {
return this._province;
}
// Province auto complete
set province(selection) {
this._province = selection;
if (!selection) return;
const country = selection.country;
if (!this.supplier.countryFk)
this.supplier.countryFk = country.id;
}
get town() {
return this._town;
}
// Town auto complete
set town(selection) {
this._town = selection;
if (!selection) return;
const province = selection.province;
const country = province.country;
const postcodes = selection.postcodes;
if (!this.supplier.provinceFk)
this.supplier.provinceFk = province.id;
if (!this.supplier.countryFk)
this.supplier.countryFk = country.id;
if (!this.supplier.postCode && postcodes.length === 1)
this.supplier.postCode = postcodes[0].code;
}
get postcode() {
return this._postcode;
}
// Postcode auto complete
set postcode(selection) {
const oldValue = this._postcode;
this._postcode = selection;
if (!selection || !oldValue) return;
const town = selection.town;
const province = town.province;
const country = province.country;
if (!this.supplier.city)
this.supplier.city = town.name;
if (!this.supplier.provinceFk)
this.supplier.provinceFk = province.id;
if (!this.supplier.countryFk)
this.supplier.countryFk = country.id;
}
onResponse(response) {
this.supplier.postCode = response.code;
this.supplier.city = response.city;
this.supplier.provinceFk = response.provinceFk;
this.supplier.countryFk = response.countryFk;
}
}
ngModule.vnComponent('vnSupplierFiscalData', {
template: require('./index.html'),
controller: Controller,
bindings: {
supplier: '<'
}
});

View File

@ -0,0 +1,109 @@
import './index';
import watcher from 'core/mocks/watcher';
describe('Supplier', () => {
describe('Component vnSupplierFiscalData', () => {
let $scope;
let $element;
let controller;
beforeEach(ngModule('supplier'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$scope.watcher = watcher;
$scope.watcher.orgData = {id: 1};
$element = angular.element('<vn-supplier-fiscal-data></supplier-fiscal-data>');
controller = $componentController('vnSupplierFiscalData', {$element, $scope});
controller.card = {reload: () => {}};
controller.supplier = {
id: 1,
name: 'Batman'
};
controller._province = {};
controller._town = {};
controller._postcode = {};
}));
describe('province() setter', () => {
it(`should set countryFk property`, () => {
controller.supplier.countryFk = null;
controller.province = {
id: 1,
name: 'New york',
country: {
id: 2,
name: 'USA'
}
};
expect(controller.supplier.countryFk).toEqual(2);
});
});
describe('town() setter', () => {
it(`should set provinceFk property`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
province: {
id: 1,
name: 'New york',
country: {
id: 2,
name: 'USA'
}
},
postcodes: []
};
expect(controller.supplier.provinceFk).toEqual(1);
});
it(`should set provinceFk property and fill the postalCode if there's just one`, () => {
controller.town = {
provinceFk: 1,
code: 46001,
province: {
id: 1,
name: 'New york',
country: {
id: 2,
name: 'USA'
}
},
postcodes: [{code: '46001'}]
};
expect(controller.supplier.provinceFk).toEqual(1);
expect(controller.supplier.postCode).toEqual('46001');
});
});
describe('postcode() setter', () => {
it(`should set the town, provinceFk and contryFk properties`, () => {
controller.postcode = {
townFk: 1,
code: 46001,
town: {
id: 1,
name: 'New York',
province: {
id: 1,
name: 'New york',
country: {
id: 2,
name: 'USA'
}
}
}
};
expect(controller.supplier.city).toEqual('New York');
expect(controller.supplier.provinceFk).toEqual(1);
expect(controller.supplier.countryFk).toEqual(2);
});
});
});
});

View File

@ -0,0 +1,3 @@
Sage tax type: Tipo de impuesto Sage
Sage transaction type: Tipo de transacción Sage
Sage withholding: Retención Sage

View File

@ -5,6 +5,8 @@ import './card';
import './descriptor';
import './index/';
import './search-panel';
import './log';
import './summary';
import './basic-data';
import './fiscal-data';
import './contact';
import './log';

View File

@ -10,6 +10,8 @@
],
"card": [
{"state": "supplier.card.basicData", "icon": "settings"},
{"state": "supplier.card.fiscalData", "icon": "account_balance"},
{"state": "supplier.card.contact", "icon": "contact_phone"},
{"state": "supplier.card.log", "icon": "history"}
]
},
@ -20,17 +22,20 @@
"abstract": true,
"component": "vn-supplier",
"description": "Suppliers"
}, {
},
{
"url": "/index?q",
"state": "supplier.index",
"component": "vn-supplier-index",
"description": "Suppliers"
}, {
},
{
"url": "/:id",
"state": "supplier.card",
"abstract": true,
"component": "vn-supplier-card"
}, {
},
{
"url": "/summary",
"state": "supplier.card.summary",
"component": "vn-supplier-summary",
@ -38,6 +43,23 @@
"params": {
"supplier": "$ctrl.supplier"
}
}, {
"url": "/basic-data",
"state": "supplier.card.basicData",
"component": "vn-supplier-basic-data",
"description": "Basic data",
"params": {
"supplier": "$ctrl.supplier"
}
}, {
"url": "/fiscal-data",
"state": "supplier.card.fiscalData",
"component": "vn-supplier-fiscal-data",
"description": "Fiscal data",
"params": {
"supplier": "$ctrl.supplier"
},
"acl": ["administrative"]
}, {
"url" : "/log",
"state": "supplier.card.log",
@ -49,6 +71,12 @@
"component": "vn-supplier-basic-data",
"description": "Basic data",
"acl": ["administrative"],
},
{
"url": "/contact",
"state": "supplier.card.contact",
"component": "vn-supplier-contact",
"description": "Contacts",
"params": {
"supplier": "$ctrl.supplier"
}

View File

@ -1,82 +1,84 @@
const app = require('vn-loopback/server/server');
describe('ticket-request filter()', () => {
it('should now return all ticket requests', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {}};
const userId = 9;
let ctx = {req: {accessToken: {userId: userId}}};
let result = await app.models.TicketRequest.filter(ctx);
it('should now return all ticket requests', async() => {
ctx.args = {};
const result = await app.models.TicketRequest.filter(ctx);
expect(result.length).toEqual(3);
});
it('should return the ticket request matching a generic search value which is the ticket ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {search: 11}};
ctx.args = {search: 11};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching a generic search value which is the client address alias', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {search: 'NY roofs'}};
ctx.args = {search: 'NY roofs'};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the ticket ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {ticketFk: 11}};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
ctx.args = {ticketFk: 11};
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the atender ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {attenderFk: 35}};
ctx.args = {attenderFk: 35};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the isOk triple-state', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {isOk: null}};
ctx.args = {isOk: null};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the client ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {clientFk: 102}};
ctx.args = {clientFk: 102};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4);
});
it('should return the ticket request matching the warehouse ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {warehouse: 1}};
ctx.args = {warehouse: 1};
let result = await app.models.TicketRequest.filter(ctx, {order: 'id'});
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx, {order: 'id'});
const requestId = result[0].id;
expect(requestId).toEqual(3);
});
it('should return the ticket request matching the salesPerson ID', async() => {
let ctx = {req: {accessToken: {userId: 9}}, args: {salesPersonFk: 18}};
ctx.args = {salesPersonFk: 18};
let result = await app.models.TicketRequest.filter(ctx);
let requestId = result[0].id;
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(3);
});

View File

@ -89,15 +89,21 @@ module.exports = Self => {
if (!zoneShipped || zoneShipped.zoneFk != zoneFk)
throw new UserError(`You don't have privileges to change the zone`);
}
const originalTicket = await models.Ticket.findById(id, {
include: {
const observationTypeDelivery = await models.ObservationType.findOne({
where: {code: 'delivery'}
});
const originalTicket = await models.Ticket.findOne({
where: {id: id},
fields: ['id', 'clientFk', 'agencyModeFk', 'addressFk', 'zoneFk',
'warehouseFk', 'companyFk', 'shipped', 'landed', 'isDeleted'],
include: [
{
relation: 'client',
scope: {
fields: 'salesPersonFk'
}
},
fields: ['id', 'clientFk', 'agencyModeFk', 'addressFk', 'zoneFk',
'warehouseFk', 'companyFk', 'shipped', 'landed', 'isDeleted']
}]
});
const updatedTicket = Object.assign({}, ctx.args);
delete updatedTicket.ctx;
@ -121,6 +127,39 @@ module.exports = Self => {
option
]);
if (originalTicket.addressFk != updatedTicket.addressFk) {
const ticketObservation = await models.TicketObservation.findOne({
where: {
ticketFk: id,
observationTypeFk: observationTypeDelivery.id}
});
if (ticketObservation)
await ticketObservation.destroy();
const address = await models.Address.findOne({
where: {id: addressFk},
include: {
relation: 'observations',
scope: {
where: {observationTypeFk: observationTypeDelivery.id},
include: {
relation: 'observationType'
}
}
}
});
const [observation] = address.observations();
if (observation) {
await models.TicketObservation.create({
ticketFk: id,
observationTypeFk: observation.observationTypeFk,
description: observation.description
});
}
}
const changes = loggable.getChanges(originalTicket, updatedTicket);
const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new);

View File

@ -5,6 +5,7 @@ describe('ticket componentUpdate()', () => {
const ticketID = 11;
const today = new Date();
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
let deliveryComponentId;
@ -97,4 +98,62 @@ describe('ticket componentUpdate()', () => {
expect(firstvalueBeforeChange).toEqual(firstvalueAfterChange);
expect(secondvalueBeforeChange).toEqual(secondvalueAfterChange);
});
it('should change the addressFk and check that delivery observations have been changed and then undo the changes', async() => {
const clientID = 102;
const addressID = 122;
const newAddressID = 2;
const agencyModeID = 8;
const warehouseID = 1;
const zoneID = 5;
const shipped = today;
const companyID = 442;
const isDeleted = false;
const landed = tomorrow;
const option = 1;
const ctx = {
args: {clientFk: clientID,
agencyModeFk: agencyModeID},
req: {
accessToken: {userId: userID},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const observationTypeDelivery = await app.models.ObservationType.findOne({
where: {code: 'delivery'}
});
const originalTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(originalTicketObservation).toBeDefined();
await app.models.Ticket.componentUpdate(ctx, ticketID, clientID, agencyModeID, newAddressID,
zoneID, warehouseID, companyID, shipped, landed, isDeleted, option);
const removedTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(removedTicketObservation).toBeNull();
// restores
await app.models.Ticket.componentUpdate(ctx, ticketID, clientID, agencyModeID, addressID,
zoneID, warehouseID, companyID, shipped, landed, isDeleted, option);
const restoredTicketObservation = await app.models.TicketObservation.findOne({
where: {
ticketFk: ticketID,
observationTypeFk: observationTypeDelivery.id}
});
expect(restoredTicketObservation.description).toEqual(originalTicketObservation.description);
});
});

27890
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"helmet": "^3.21.2",
"i18n": "^0.8.4",
"imap": "^0.8.19",
"ldapjs": "^1.0.2",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
"loopback-boot": "^2.27.1",
"loopback-component-explorer": "^6.5.0",

1743
print/package-lock.json generated

File diff suppressed because it is too large Load Diff