Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2568-ticket_basic_data_e2e_fix

This commit is contained in:
Carlos Jimenez Ruiz 2020-11-05 09:08:35 +01:00
commit 01ccef4786
51 changed files with 1708 additions and 270 deletions

View File

@ -2,15 +2,11 @@ const app = require('vn-loopback/server/server');
describe('campaign upcoming()', () => { describe('campaign upcoming()', () => {
it('should return the upcoming campaign but from the last year', async() => { it('should return the upcoming campaign but from the last year', async() => {
let response = await app.models.Campaign.upcoming(); const response = await app.models.Campaign.upcoming();
const lastYearDate = new Date();
lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);
const lastYear = lastYearDate.getFullYear();
const campaignDated = response.dated; 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": { "Sip": {
"dataSource": "vn" "dataSource": "vn"
}, },
"SageWithholding": {
"dataSource": "vn"
},
"UserConfigView": { "UserConfigView": {
"dataSource": "vn" "dataSource": "vn"
}, },

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,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,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Supplier', 'updateFiscalData', 'WRITE', 'ALLOW', 'ROLE', 'administrative');
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 ('SupplierLog', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -604,6 +604,24 @@ INSERT INTO `TiposTransacciones` VALUES (1,'Rég.general/Oper.interiores bienes
UNLOCK TABLES; UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!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 */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;

View File

@ -1211,11 +1211,11 @@ INSERT INTO `vn`.`annualAverageInvoiced`(`clientFk`, `invoiced`)
(104, 500), (104, 500),
(105, 5000); (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 VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, 0, CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 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, 'B22222222', 1, 0, CURDATE(), 1, 'supplier address 2', 'SILLA', 2, 43022, 1, 2, 10, 93, 8), (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, 'C33333333', 0, 0, CURDATE(), 1, 'supplier address 3', 'SILLA', 1, 43022, 1, 2, 15, NULL, NULL); (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`) INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES VALUES

View File

@ -923,5 +923,20 @@ export default {
thirdContactNotes: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.observation"]', thirdContactNotes: 'vn-supplier-contact div:nth-child(3) vn-textfield[ng-model="contact.observation"]',
saveButton: 'vn-supplier-contact button[type="submit"]', saveButton: 'vn-supplier-contact button[type="submit"]',
thirdContactDeleteButton: 'vn-supplier-contact div:nth-child(3) vn-icon-button[icon="delete"]' 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

@ -57,6 +57,8 @@ describe('Client lock verified data path', () => {
}); });
it('should check the Verified data checkbox', async() => { 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.verifiedDataCheckbox);
await page.waitToClick(selectors.clientFiscalData.saveButton); await page.waitToClick(selectors.clientFiscalData.saveButton);
await page.waitToClick(selectors.globalItems.acceptButton); await page.waitToClick(selectors.globalItems.acceptButton);

View File

@ -79,6 +79,6 @@ describe('Supplier summary & descriptor path', () => {
}); });
it(`should check the client button isn't present since this supplier should not be a client`, async() => { 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,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');
});
});

View File

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

View File

@ -81,5 +81,8 @@
"shipped": "Shipped", "shipped": "Shipped",
"landed": "Landed", "landed": "Landed",
"addressFk": "Address", "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", "shipped": "F. envío",
"landed": "F. entrega", "landed": "F. entrega",
"addressFk": "Consignatario", "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 => { module.exports = Self => {
Self.remoteMethod('sync', { Self.remoteMethod('sync', {
@ -10,119 +11,17 @@ module.exports = Self => {
}); });
Self.sync = async function() { Self.sync = async function() {
let $ = Self.app.models; let engine = new SyncEngine();
await engine.init(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 err; let err;
try { try {
// Delete roles await engine.syncRoles();
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};
});
reqs = [];
for (let role of roles) {
let newEntry = {
objectClass: ['top', 'posixGroup'],
cn: role.name,
gidNumber: accountConfig.idBase + role.id
};
let memberUid = [];
for (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);
} catch (e) { } catch (e) {
err = e; err = e;
} }
await client.unbind(); await engine.deinit();
if (err) throw err; if (err) throw err;
}; };
}; };
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

@ -13,25 +13,24 @@ module.exports = Self => {
Self.syncAll = async function() { Self.syncAll = async function() {
let $ = Self.app.models; let $ = Self.app.models;
let se = new SyncEngine(); let engine = new SyncEngine();
await se.init($); await engine.init($);
let usersToSync = await se.getUsers(); let usersToSync = await engine.getUsers();
usersToSync = Array.from(usersToSync.values()) usersToSync = Array.from(usersToSync.values())
.sort((a, b) => a.localeCompare(b)); .sort((a, b) => a.localeCompare(b));
for (let user of usersToSync) { for (let user of usersToSync) {
try { try {
console.log(`Synchronizing user '${user}'`); console.log(`Synchronizing user '${user}'`);
await se.sync(user); await engine.sync(user);
console.log(` -> '${user}' sinchronized`); console.log(` -> '${user}' sinchronized`);
} catch (err) { } catch (err) {
console.error(` -> '${user}' synchronization error:`, err.message); console.error(` -> '${user}' synchronization error:`, err.message);
} }
} }
await se.deinit(); await engine.deinit();
await $.RoleInherit.sync(); await $.RoleInherit.sync();
}; };
}; };

View File

@ -34,17 +34,17 @@ module.exports = Self => {
if (user && isSync) return; if (user && isSync) return;
let err; let err;
let se = new SyncEngine(); let engine = new SyncEngine();
await se.init($); await engine.init($);
try { try {
await se.sync(userName, password, true); await engine.sync(userName, password, true);
await $.UserSync.destroyById(userName); await $.UserSync.destroyById(userName);
} catch (e) { } catch (e) {
err = e; err = e;
} }
await se.deinit(); await engine.deinit();
if (err) throw err; if (err) throw err;
}; };
}; };

View File

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

View File

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

View File

@ -37,6 +37,11 @@ class SyncConnector {
*/ */
async syncGroups(user, userName) {} async syncGroups(user, userName) {}
/**
* Synchronizes roles.
*/
async syncRoles() {}
/** /**
* Deinitalizes the connector. * Deinitalizes the connector.
*/ */

View File

@ -19,8 +19,10 @@ module.exports = class SyncEngine {
for (let ConnectorClass of SyncConnector.connectors) { for (let ConnectorClass of SyncConnector.connectors) {
let connector = new ConnectorClass(); let connector = new ConnectorClass();
Object.assign(connector, { Object.assign(connector, {
se: this, engine: this,
$ $,
accountConfig,
mailConfig
}); });
if (!await connector.init()) continue; if (!await connector.init()) continue;
connectors.push(connector); connectors.push(connector);
@ -80,27 +82,20 @@ module.exports = class SyncEngine {
} }
}); });
let extraParams;
let hasAccount = false;
if (user) {
hasAccount = user.active
&& await $.UserAccount.exists(user.id);
extraParams = {
corporateMail: `${userName}@${mailConfig.domain}`,
uidNumber: accountConfig.idBase + user.id
};
}
let info = { let info = {
user, user,
extraParams, hasAccount: false
hasAccount,
accountConfig,
mailConfig
}; };
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 = []; let errs = [];
for (let connector of this.connectors) { for (let connector of this.connectors) {
@ -116,6 +111,11 @@ module.exports = class SyncEngine {
if (errs.length) throw errs[0]; if (errs.length) throw errs[0];
} }
async syncRoles() {
for (let connector of this.connectors)
await connector.syncRoles();
}
async getUsers() { async getUsers() {
let usersToSync = new Set(); let usersToSync = new Set();

View File

@ -7,18 +7,20 @@ const crypto = require('crypto');
class SyncLdap extends SyncConnector { class SyncLdap extends SyncConnector {
async init() { async init() {
let ldapConfig = await this.$.LdapConfig.findOne({ let ldapConfig = await this.$.LdapConfig.findOne({
fields: ['host', 'rdn', 'password', 'baseDn', 'groupDn'] fields: [
'server',
'rdn',
'password',
'userDn',
'groupDn'
]
}); });
if (!ldapConfig) return false; if (!ldapConfig) return false;
let client = ldap.createClient({ let client = ldap.createClient({
url: `ldap://${ldapConfig.host}:389` url: ldapConfig.server
}); });
await client.bind(ldapConfig.rdn, ldapConfig.password);
let ldapPassword = Buffer
.from(ldapConfig.password, 'base64')
.toString('ascii');
await client.bind(ldapConfig.rdn, ldapPassword);
Object.assign(this, { Object.assign(this, {
ldapConfig, ldapConfig,
@ -36,16 +38,12 @@ class SyncLdap extends SyncConnector {
let { let {
ldapConfig, ldapConfig,
client, client,
accountConfig
} = this; } = this;
let { let {user} = info;
user,
hasAccount,
extraParams,
accountConfig
} = info;
let res = await client.search(ldapConfig.baseDn, { let res = await client.search(ldapConfig.userDn, {
scope: 'sub', scope: 'sub',
attributes: ['userPassword', 'sambaNTPassword'], attributes: ['userPassword', 'sambaNTPassword'],
filter: `&(uid=${userName})` filter: `&(uid=${userName})`
@ -59,13 +57,13 @@ class SyncLdap extends SyncConnector {
}); });
try { try {
let dn = `uid=${userName},${ldapConfig.baseDn}`; let dn = `uid=${userName},${ldapConfig.userDn}`;
await client.del(dn); await client.del(dn);
} catch (e) { } catch (e) {
if (e.name !== 'NoSuchObjectError') throw e; if (e.name !== 'NoSuchObjectError') throw e;
} }
if (!hasAccount) { if (!info.hasAccount) {
if (oldUser) if (oldUser)
console.log(` -> '${userName}' removed from LDAP`); console.log(` -> '${userName}' removed from LDAP`);
return; return;
@ -77,7 +75,7 @@ class SyncLdap extends SyncConnector {
? nameArgs.splice(1).join(' ') ? nameArgs.splice(1).join(' ')
: '-'; : '-';
let dn = `uid=${userName},${ldapConfig.baseDn}`; let dn = `uid=${userName},${ldapConfig.userDn}`;
let newEntry = { let newEntry = {
uid: userName, uid: userName,
objectClass: [ objectClass: [
@ -89,11 +87,11 @@ class SyncLdap extends SyncConnector {
displayName: nickname, displayName: nickname,
givenName: nameArgs[0], givenName: nameArgs[0],
sn, sn,
mail: extraParams.corporateMail, mail: info.corporateMail,
preferredLanguage: user.lang, preferredLanguage: user.lang,
homeDirectory: `${accountConfig.homedir}/${userName}`, homeDirectory: `${accountConfig.homedir}/${userName}`,
loginShell: accountConfig.shell, loginShell: accountConfig.shell,
uidNumber: extraParams.uidNumber, uidNumber: info.uidNumber,
gidNumber: accountConfig.idBase + user.roleFk, gidNumber: accountConfig.idBase + user.roleFk,
sambaSID: '-' sambaSID: '-'
}; };
@ -137,11 +135,6 @@ class SyncLdap extends SyncConnector {
client client
} = this; } = this;
let {
user,
hasAccount
} = info;
let res = await client.search(ldapConfig.groupDn, { let res = await client.search(ldapConfig.groupDn, {
scope: 'sub', scope: 'sub',
attributes: ['dn'], attributes: ['dn'],
@ -165,10 +158,10 @@ class SyncLdap extends SyncConnector {
} }
await Promise.all(reqs); await Promise.all(reqs);
if (!hasAccount) return; if (!info.hasAccount) return;
reqs = []; reqs = [];
for (let role of user.roles()) { for (let role of info.user.roles()) {
let change = new ldap.Change({ let change = new ldap.Change({
operation: 'add', operation: 'add',
modification: {memberUid: userName} modification: {memberUid: userName}
@ -186,7 +179,7 @@ class SyncLdap extends SyncConnector {
client client
} = this; } = this;
let res = await client.search(ldapConfig.baseDn, { let res = await client.search(ldapConfig.userDn, {
scope: 'sub', scope: 'sub',
attributes: ['uid'], attributes: ['uid'],
filter: `uid=*` filter: `uid=*`
@ -198,7 +191,103 @@ class SyncLdap extends SyncConnector {
res.on('end', resolve); 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); SyncConnector.connectors.push(SyncLdap);
module.exports = 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

@ -1,76 +1,79 @@
const SyncConnector = require('./sync-connector'); const SyncConnector = require('./sync-connector');
const ssh = require('node-ssh'); const ssh = require('node-ssh');
const ldap = require('./ldapjs-extra');
class SyncSamba extends SyncConnector { class SyncSamba extends SyncConnector {
async init() { async init() {
let sambaConfig = await this.$.SambaConfig.findOne({ let sambaConfig = await this.$.SambaConfig.findOne({
fields: ['host', 'sshUser', 'sshPass'] fields: [
'host',
'sshUser',
'sshPassword',
'adUser',
'adPassword',
'userDn'
]
}); });
if (!sambaConfig) return false; if (!sambaConfig) return false;
let sshPassword = Buffer
.from(sambaConfig.sshPass, 'base64')
.toString('ascii');
let client = new ssh.NodeSSH(); let client = new ssh.NodeSSH();
await client.connect({ await client.connect({
host: sambaConfig.host, host: sambaConfig.host,
username: sambaConfig.sshUser, username: sambaConfig.sshUser,
password: sshPassword password: sambaConfig.sshPassword
});
let adClient = ldap.createClient({
url: `ldaps://${sambaConfig.host}:636`,
tlsOptions: {rejectUnauthorized: false}
}); });
Object.assign(this, { Object.assign(this, {
sambaConfig, sambaConfig,
client client,
adClient
}); });
return true; return true;
} }
async deinit() { async deinit() {
if (this.client) if (!this.client) return;
await this.client.dispose(); await this.client.dispose();
await this.adClient.unbind();
} }
async sync(info, userName, password) { async sync(info, userName, password) {
let { let {client} = this;
client
} = this;
let { if (info.hasAccount) {
hasAccount,
extraParams
} = info;
if (hasAccount) {
try { try {
await client.exec('samba-tool user create', [ await client.exec('samba-tool user create', [
userName, userName,
'--uid-number', `${extraParams.uidNumber}`, '--uid-number', `${info.uidNumber}`,
'--mail-address', extraParams.corporateMail, '--mail-address', info.corporateMail,
'--random-password' '--random-password'
]); ]);
} catch (e) {}
await client.exec('samba-tool user setexpiry', [ await client.exec('samba-tool user setexpiry', [
userName, userName,
'--noexpiry' '--noexpiry'
]); ]);
await client.exec('mkhomedir_helper', [
userName,
'0027'
]);
} catch (e) {}
await client.exec('samba-tool user enable', [
userName
]);
if (password) { if (password) {
await client.exec('samba-tool user setpassword', [ await client.exec('samba-tool user setpassword', [
userName, userName,
'--newpassword', password '--newpassword', password
]); ]);
await client.exec('samba-tool user enable', [
userName
]);
} }
await client.exec('mkhomedir_helper', [
userName,
'0027'
]);
} else { } else {
try { try {
await client.exec('samba-tool user disable', [ await client.exec('samba-tool user disable', [
@ -81,11 +84,40 @@ class SyncSamba extends SyncConnector {
} }
} }
/**
* 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) { async getUsers(usersToSync) {
let {client} = this; let {
let res = await client.execCommand('samba-tool user list'); sambaConfig,
let users = res.stdout.split('\n'); adClient
for (let user of users) usersToSync.add(user.trim()); } = 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);
});
} }
} }

View File

@ -11,9 +11,17 @@
class="vn-w-md"> class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-vertical> <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 <vn-textfield
label="Host" label="Server"
ng-model="$ctrl.config.host" ng-model="$ctrl.config.server"
rule="LdapConfig" rule="LdapConfig"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
@ -25,18 +33,12 @@
<vn-textfield <vn-textfield
label="Password" label="Password"
ng-model="$ctrl.config.password" ng-model="$ctrl.config.password"
info="Password should be base64 encoded"
type="password" type="password"
rule="LdapConfig"> rule="LdapConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Base DN" label="User DN"
ng-model="$ctrl.config.baseDn" ng-model="$ctrl.config.userDn"
rule="LdapConfig">
</vn-textfield>
<vn-textfield
label="Filter"
ng-model="$ctrl.config.filter"
rule="LdapConfig"> rule="LdapConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield

View File

@ -6,7 +6,7 @@ export default class Controller extends Section {
onSynchronizeAll() { onSynchronizeAll() {
this.vnApp.showSuccess(this.$t('Synchronizing in the background')); this.vnApp.showSuccess(this.$t('Synchronizing in the background'));
this.$http.patch(`UserAccounts/syncAll`) this.$http.patch(`UserAccounts/syncAll`)
.then(() => this.vnApp.showSuccess(this.$t('LDAP users synchronized'))); .then(() => this.vnApp.showSuccess(this.$t('Users synchronized!')));
} }
onUserSync() { onUserSync() {
@ -15,7 +15,7 @@ export default class Controller extends Section {
let params = {password: this.syncPassword}; let params = {password: this.syncPassword};
return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params) return this.$http.patch(`UserAccounts/${this.syncUser}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized'))); .then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
} }
onSyncClose() { onSyncClose() {

View File

@ -1,7 +1,7 @@
Host: Host Enable synchronization: Habilitar sincronización
Server: Servidor
RDN: RDN RDN: RDN
Base DN: DN base User DN: DN usuarios
Password should be base64 encoded: La contraseña debe estar codificada en base64
Filter: Filtro Filter: Filtro
Group DN: DN grupos Group DN: DN grupos
Synchronize now: Sincronizar ahora Synchronize now: Sincronizar ahora
@ -9,8 +9,8 @@ Synchronize user: Sincronizar usuario
If password is not specified, just user attributes are synchronized: >- 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 Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
Synchronizing in the background: Sincronizando en segundo plano Synchronizing in the background: Sincronizando en segundo plano
LDAP users synchronized: Usuarios LDAP sincronizados Users synchronized!: ¡Usuarios sincronizados!
Username: Nombre de usuario Username: Nombre de usuario
Synchronize: Sincronizar Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario Please enter the username: Por favor introduce el nombre de usuario
User synchronized: Usuario sincronizado User synchronized!: ¡Usuario sincronizado!

View File

@ -11,24 +11,47 @@
class="vn-w-md"> class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-vertical> <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 <vn-textfield
label="SSH host" label="Host"
ng-model="$ctrl.config.host" ng-model="$ctrl.config.host"
rule="SambaConfig" rule="SambaConfig"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="User" label="SSH user"
ng-model="$ctrl.config.sshUser" ng-model="$ctrl.config.sshUser"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Password" label="SSH password"
ng-model="$ctrl.config.sshPass" ng-model="$ctrl.config.sshPassword"
info="Password should be base64 encoded"
type="password" type="password"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </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-vertical>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -1,2 +1,7 @@
SSH host: Host SSH Enable synchronization: Habilitar sincronización
Password should be base64 encoded: La contraseña debe estar codificada en base64 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

@ -2,9 +2,11 @@ const app = require('vn-loopback/server/server');
describe('Client updateFiscalData', () => { describe('Client updateFiscalData', () => {
const clientId = 101; const clientId = 101;
const employeeId = 1;
const salesAssistantId = 21;
const administrativeId = 5;
afterAll(async done => { afterAll(async done => {
const clientId = 101; const ctx = {req: {accessToken: {userId: administrativeId}}};
const ctx = {req: {accessToken: {userId: 5}}};
ctx.args = {postcode: 46460}; ctx.args = {postcode: 46460};
await app.models.Client.updateFiscalData(ctx, clientId); await app.models.Client.updateFiscalData(ctx, clientId);
@ -12,8 +14,8 @@ describe('Client updateFiscalData', () => {
done(); done();
}); });
it('should return an error if the user is not administrative and the isTaxDataChecked value is true', async() => { it('should return an error if the user is not salesAssistant and the isTaxDataChecked value is true', async() => {
const ctx = {req: {accessToken: {userId: 1}}}; const ctx = {req: {accessToken: {userId: employeeId}}};
ctx.args = {}; ctx.args = {};
let error; let error;
@ -22,11 +24,30 @@ describe('Client updateFiscalData', () => {
error = e; 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() => { 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}; ctx.args = {postcode: 46680};
const client = await app.models.Client.findById(clientId); const client = await app.models.Client.findById(clientId);

View File

@ -45,15 +45,15 @@ module.exports = Self => {
}, },
{ {
arg: 'sageTaxTypeFk', arg: 'sageTaxTypeFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'sageTransactionTypeFk', arg: 'sageTransactionTypeFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'transferorFk', arg: 'transferorFk',
type: 'number' type: 'any'
}, },
{ {
arg: 'hasToInvoiceByAddress', arg: 'hasToInvoiceByAddress',
@ -118,6 +118,15 @@ module.exports = Self => {
if (!isSalesAssistant && client.isTaxDataChecked) if (!isSalesAssistant && client.isTaxDataChecked)
throw new UserError(`You can't make changes on a client with verified data`); 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) { if (args.despiteOfClient) {
const logRecord = { const logRecord = {
originFk: clientId, originFk: clientId,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1,4 +1,85 @@
const UserError = require('vn-loopback/util/user-error');
const validateTin = require('vn-loopback/util/validateTin');
module.exports = Self => { module.exports = Self => {
require('../methods/supplier/filter')(Self); require('../methods/supplier/filter')(Self);
require('../methods/supplier/getSummary')(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" "type": "String"
}, },
"account": { "account": {
"type": "Number" "type": "String"
}, },
"countryFk": { "countryFk": {
"type": "Number" "type": "Number"
@ -64,7 +64,7 @@
"type": "Number" "type": "Number"
}, },
"postCode": { "postCode": {
"type": "Number" "type": "String"
}, },
"payMethodFk": { "payMethodFk": {
"type": "Number" "type": "Number"
@ -77,6 +77,24 @@
}, },
"nickname": { "nickname": {
"type": "String" "type": "String"
},
"sageTaxTypeFk": {
"type": "number",
"mysql": {
"columnName": "taxTypeSageFk"
}
},
"sageTransactionTypeFk": {
"type": "number",
"mysql": {
"columnName": "transactionTypeSageFk"
}
},
"sageWithholdingFk": {
"type": "number",
"mysql": {
"columnName": "withholdingSageFk"
}
} }
}, },
"relations": { "relations": {

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,7 @@ import './card';
import './descriptor'; import './descriptor';
import './index/'; import './index/';
import './search-panel'; import './search-panel';
import './log';
import './summary'; import './summary';
import './fiscal-data';
import './contact'; import './contact';
import './log';

View File

@ -9,6 +9,8 @@
{"state": "supplier.index", "icon": "icon-supplier"} {"state": "supplier.index", "icon": "icon-supplier"}
], ],
"card": [ "card": [
{"state": "supplier.card.basicData", "icon": "settings"},
{"state": "supplier.card.fiscalData", "icon": "account_balance"},
{"state": "supplier.card.contact", "icon": "contact_phone"}, {"state": "supplier.card.contact", "icon": "contact_phone"},
{"state": "supplier.card.log", "icon": "history"} {"state": "supplier.card.log", "icon": "history"}
] ]
@ -41,8 +43,24 @@
"params": { "params": {
"supplier": "$ctrl.supplier" "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", "url" : "/log",
"state": "supplier.card.log", "state": "supplier.card.log",
"component": "vn-supplier-log", "component": "vn-supplier-log",