Merge branch 'dev' into 5090-renameGoLabel
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Carlos Satorres 2023-03-02 10:44:24 +00:00
commit ea9a15ece1
73 changed files with 1146 additions and 166 deletions

View File

@ -5,10 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2308.01] - 2023-03-09 ## [2310.01] - 2023-03-23
### Added ### Added
- (Client -> Descriptor) Nuevo icono $ con barrotes para los clientes con impago -
### Changed ### Changed
- -
@ -16,6 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- -
## [2308.01] - 2023-03-09
### Added
- (Proveedores -> Datos fiscales) Añadido checkbox 'Vies'
- (Client -> Descriptor) Nuevo icono $ con barrotes para los clientes con impago
- (Trabajador -> Datos Básicos) Añadido nuevo campo Taquilla
- (Trabajador -> PDA) Nueva sección
### Changed
- (Ticket -> Borrar ticket) Restringido el borrado de tickets con abono
## [2306.01] - 2023-02-23 ## [2306.01] - 2023-02-23
### Added ### Added

View File

@ -30,7 +30,10 @@ async function test() {
const bootOptions = {dataSources}; const bootOptions = {dataSources};
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
app.boot(bootOptions); await new Promise((resolve, reject) => {
app.boot(bootOptions,
err => err ? reject(err) : resolve());
});
const Jasmine = require('jasmine'); const Jasmine = require('jasmine');
const jasmine = new Jasmine(); const jasmine = new Jasmine();

View File

@ -0,0 +1,16 @@
ALTER TABLE `vn`.`supplier` ADD `isVies` tinyint(4) DEFAULT 0 NOT NULL;
UPDATE `vn`.`supplier` s
JOIN vn.country c ON c.id = s.countryFk
SET s.nif = MID(s.nif, 3, LENGTH(s.nif)-1), s.isVies = TRUE
WHERE s.nif <> TRIM(IF(c.code = LEFT(s.nif, 2), MID(s.nif, 3, LENGTH(s.nif)-1), s.nif));
INSERT IGNORE INTO `vn`.`chat`
(senderFk, recipient, checkUserStatus, message, status, attempts)
VALUES(19263, '#informatica-cau', 0, '
```
UPDATE `vn`.`supplier` s
JOIN vn.country c ON c.id = s.countryFk
SET s.nif = MID(s.nif, 3, LENGTH(s.nif)-1), s.isVies = TRUE
WHERE s.nif <> TRIM(IF(c.code = LEFT(s.nif, 2), MID(s.nif, 3, LENGTH(s.nif)-1), s.nif));
```', 0, 0);

View File

@ -0,0 +1,15 @@
ALTER TABLE `vn`.`worker` ADD locker INT UNSIGNED NULL UNIQUE;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('DeviceProduction', '*', '*', 'ALLOW', 'ROLE', 'hr'),
('DeviceProductionModels', '*', '*', 'ALLOW', 'ROLE', 'hr'),
('DeviceProductionState', '*', '*', 'ALLOW', 'ROLE', 'hr'),
('DeviceProductionUser', '*', '*', 'ALLOW', 'ROLE', 'hr'),
('DeviceProduction', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('DeviceProductionModels', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('DeviceProductionState', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('DeviceProductionUser', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('Worker', 'deallocatePDA', '*', 'ALLOW', 'ROLE', 'hr'),
('Worker', 'allocatePDA', '*', 'ALLOW', 'ROLE', 'hr'),
('Worker', 'deallocatePDA', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('Worker', 'allocatePDA', '*', 'ALLOW', 'ROLE', 'productionAssi');

View File

@ -0,0 +1,127 @@
DROP PROCEDURE IF EXISTS `sage`.`clientSupplier_add`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `sage`.`clientSupplier_add`(vCompanyFk INT)
BEGIN
/**
* Prepara los datos de clientes y proveedores para exportarlos a Sage
* @vCompanyFk Empresa dela que se quiere trasladar datos
*/
DECLARE vCountryCeutaMelillaFk INT;
DECLARE vCountryCanariasCode, vCountryCeutaMelillaCode VARCHAR(2);
SELECT SiglaNacion INTO vCountryCanariasCode
FROM Naciones
WHERE Nacion ='ISLAS CANARIAS';
SELECT CodigoNacion, SiglaNacion INTO vCountryCeutaMelillaFk, vCountryCeutaMelillaCode
FROM Naciones
WHERE Nacion ='CEUTA Y MELILLA';
TRUNCATE TABLE clientesProveedores;
INSERT INTO clientesProveedores
(CodigoEmpresa,
ClienteOProveedor,
CodigoClienteProveedor,
RazonSocial,
Nombre,
Domicilio,
CodigoCuenta,
CifDni,
CifEuropeo,
CodigoPostal,
Municipio,
CodigoProvincia,
Provincia,
CodigoNacion,
SiglaNacion,
PersonaFisicaJuridica,
TipoDocumentoPersona,
CodigoIva,
Nacion,
Telefono,
Telefono2,
CodigoTransaccion,
CodigoRetencion,
Email1,
iban)
SELECT
company_getCode(vCompanyFk),
'C',
c.id,
c.socialName,
c.socialName,
IFNULL(c.street, ''),
c.accountingAccount,
TRIM(IF(c.isVies, CONCAT(cu.code,c.fi), c.fi)),
IF(n.NacionCEE,TRIM(IF(cu.code = LEFT(c.fi, 2), c.fi, CONCAT(cu.code,c.fi))) , ''),
IFNULL(c.postcode, ''),
IFNULL(c.city, ''),
IFNULL(pr.CodigoProvincia, ''),
IFNULL(p.name, ''),
IF(n.SiglaNacion = vCountryCanariasCode COLLATE utf8mb3_unicode_ci, IF(@isCeutaMelilla := IF(pr.Provincia IN ('CEUTA', 'MELILLA'), TRUE, FALSE), vCountryCeutaMelillaFk, IF (@isCanarias, vCountryCanariasCode, n.CodigoNacion)), n.CodigoNacion),
IF(n.SiglaNacion = vCountryCanariasCode COLLATE utf8mb3_unicode_ci, IF(@isCeutaMelilla, vCountryCeutaMelillaCode, IF (@isCanarias, vCountryCanariasCode, n.SiglaNacion)), n.SiglaNacion),
IF((c.fi REGEXP '^([[:blank:]]|[[:digit:]])'), 'J','F'),
IF(cu.code IN('ES','EX'),
1,
IF((cu.isUeeMember AND c.isVies), 2, 4)),
IFNULL(c.taxTypeSageFk,0),
IF(n.SiglaNacion = vCountryCanariasCode COLLATE utf8mb3_unicode_ci,
IF(@isCeutaMelilla, 'CEUTA Y MELILLA', IF (@isCanarias, 'ISLAS CANARIAS', n.Nacion)),
n.Nacion),
IFNULL(c.phone, ''),
IFNULL(c.mobile, ''),
IFNULL(c.transactionTypeSageFk, 0),
'0',
IFNULL(SUBSTR(c.email, 1, LOCATE(',', CONCAT(c.email, ','))-1), ''),
IFNULL(c.iban, '')
FROM vn.`client` c
JOIN clientLastTwoMonths clm ON clm.clientFk = c.id
LEFT JOIN vn.country cu ON cu.id = c.countryFk
LEFT JOIN Naciones n ON n.countryFk = cu.id
LEFT JOIN vn.province p ON p.id = c.provinceFk
LEFT JOIN Provincias pr ON pr.provinceFk = p.id
WHERE c.isRelevant
AND clm.companyFk = vCompanyFk
UNION ALL
SELECT company_getCode(vCompanyFk),
'P',
s.id,
s.name,
s.name,
IFNULL(s.street, ''),
s.account,
TRIM(IF(s.isVies, CONCAT(co.code,s.nif), s.nif)),
IF(n.NacionCEE, TRIM(CONCAT(co.code, IF(co.code = LEFT(s.nif, 2), MID(s.nif, 3, LENGTH(s.nif) - 1), s.nif))), ''),
IFNULL(s.postCode,''),
IFNULL(s.city, ''),
IFNULL(pr.CodigoProvincia, ''),
IFNULL(p.name, ''),
n.CodigoNacion,
n.SiglaNacion COLLATE utf8mb3_unicode_ci,
IF((s.nif REGEXP '^([[:blank:]]|[[:digit:]])'),'J','F'),
IF(co.country IN ('España', 'España exento'), 1,IF(co.isUeeMember = 1, 2, 4)),
IFNULL(s.taxTypeSageFk, 0),
n.Nacion,
IFNULL(sc.phone, ''),
IFNULL(sc.mobile, ''),
IFNULL(s.transactionTypeSageFk, 0),
IFNULL(s.withholdingSageFk, '0'),
IFNULL(SUBSTR(sc.email, 1, (COALESCE(NULLIF(LOCATE(',', sc.email), 0), 99) - 1)), ''),
IFNULL(iban, '')
FROM vn.supplier s
JOIN supplierLastThreeMonths pl ON pl.supplierFk = s.id
LEFT JOIN vn.country co ON co.id = s.countryFk
LEFT JOIN Naciones n ON n.countryFk = co.id
LEFT JOIN vn.province p ON p.id = s.provinceFk
LEFT JOIN Provincias pr ON pr.provinceFk = p.id
LEFT JOIN vn.supplierContact sc ON sc.supplierFk = s.id
LEFT JOIN vn.supplierAccount sa ON sa.supplierFk = s.id
WHERE pl.companyFk = vCompanyFk AND
s.isActive AND
s.nif <> ''
GROUP BY pl.supplierFk, pl.companyFk;
END$$
DELIMITER ;

View File

@ -2787,3 +2787,34 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`)
VALUES VALUES
(1, NULL, 1); (1, NULL, 1);
INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`)
VALUES
(1, 12);
INSERT INTO `vn`.`deviceProductionModels` (`code`)
VALUES
('BLACKVIEW'),
('DODGEE'),
('ZEBRA');
INSERT INTO `vn`.`deviceProductionState` (`code`, `description`)
VALUES
('active', 'activo'),
('idle', 'inactivo'),
('lost', 'perdida'),
('repair', 'reparación'),
('retired', 'retirada');
INSERT INTO `vn`.`deviceProduction` (`imei`, `modelFk`, `macWifi`, `serialNumber`, `android_id`, `purchased`, `stateFk`, `isInScalefusion`, `description`)
VALUES
('ime1', 'BLACKVIEW', 'macWifi1', 'serialNumber1', 'android_id1', util.VN_NOW(), 'active', 0, NULL),
('ime2', 'DODGEE', 'macWifi2', 'serialNumber2', 'android_id2', util.VN_NOW(), 'idle', 0, NULL),
('ime3', 'ZEBRA', 'macWifi3', 'serialNumber3', 'android_id3', util.VN_NOW(), 'active', 0, NULL),
('ime4', 'BLACKVIEW', 'macWifi4', 'serialNumber4', 'android_id4', util.VN_NOW(), 'idle', 0, NULL);
INSERT INTO `vn`.`deviceProductionUser` (`deviceProductionFk`, `userFk`, `created`)
VALUES
(1, 1, util.VN_NOW()),
(3, 3, util.VN_NOW());

View File

@ -22,7 +22,7 @@ describe('ticket ticketCalculateClon()', () => {
originalTicketId: 11 originalTicketId: 11
}; };
stmt = new ParameterizedSQL('CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @result)', [ stmt = new ParameterizedSQL('CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @result)', [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -31,7 +31,8 @@ describe('ticket ticketCalculateClon()', () => {
params.agencyType, params.agencyType,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);
@ -71,7 +72,7 @@ describe('ticket ticketCalculateClon()', () => {
originalTicketId: 11 originalTicketId: 11
}; };
stmt = new ParameterizedSQL('CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @result)', [ stmt = new ParameterizedSQL('CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @result)', [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -80,7 +81,8 @@ describe('ticket ticketCalculateClon()', () => {
params.agencyType, params.agencyType,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);

View File

@ -1,7 +1,7 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
describe('ticket ticketCreateWithUser()', () => { describe('ticket ticket_add()', () => {
const today = Date.vnNew(); const today = Date.vnNew();
it('should confirm the procedure creates the expected ticket', async() => { it('should confirm the procedure creates the expected ticket', async() => {
let stmts = []; let stmts = [];
@ -21,7 +21,7 @@ describe('ticket ticketCreateWithUser()', () => {
userId: 18 userId: 18
}; };
stmt = new ParameterizedSQL(`CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [ stmt = new ParameterizedSQL(`CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -30,7 +30,8 @@ describe('ticket ticketCreateWithUser()', () => {
params.agencyModeFk, params.agencyModeFk,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);
@ -70,7 +71,7 @@ describe('ticket ticketCreateWithUser()', () => {
userId: 18 userId: 18
}; };
stmt = new ParameterizedSQL('CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)', [ stmt = new ParameterizedSQL('CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)', [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -79,7 +80,8 @@ describe('ticket ticketCreateWithUser()', () => {
params.agencyModeFk, params.agencyModeFk,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);
@ -120,7 +122,7 @@ describe('ticket ticketCreateWithUser()', () => {
userId: 18 userId: 18
}; };
stmt = new ParameterizedSQL(`CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [ stmt = new ParameterizedSQL(`CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -129,7 +131,8 @@ describe('ticket ticketCreateWithUser()', () => {
params.agencyModeFk, params.agencyModeFk,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);
@ -172,7 +175,7 @@ describe('ticket ticketCreateWithUser()', () => {
]); ]);
stmts.push(stmt); stmts.push(stmt);
stmt = new ParameterizedSQL(`CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [ stmt = new ParameterizedSQL(`CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @newTicketId)`, [
params.clientFk, params.clientFk,
params.shipped, params.shipped,
params.warehouseFk, params.warehouseFk,
@ -181,7 +184,8 @@ describe('ticket ticketCreateWithUser()', () => {
params.agencyModeFk, params.agencyModeFk,
params.routeFk, params.routeFk,
params.landed, params.landed,
params.userId params.userId,
true
]); ]);
stmts.push(stmt); stmts.push(stmt);
stmts.push(`select @newTicketId`); stmts.push(`select @newTicketId`);

View File

@ -22,6 +22,7 @@ export default {
userConfigSecondAutocomplete: '#localBank', userConfigSecondAutocomplete: '#localBank',
userConfigThirdAutocomplete: '#localCompany', userConfigThirdAutocomplete: '#localCompany',
acceptButton: '.vn-confirm.shown button[response=accept]', acceptButton: '.vn-confirm.shown button[response=accept]',
cancelButton: '.vn-confirm.shown input[response=cancel]',
searchButton: 'vn-searchbar vn-icon[icon="search"]' searchButton: 'vn-searchbar vn-icon[icon="search"]'
}, },
moduleIndex: { moduleIndex: {
@ -973,6 +974,7 @@ export default {
id: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(3) > section > span', id: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(3) > section > span',
email: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(4) > section > span', email: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(4) > section > span',
department: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(5) > section > span', department: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(5) > section > span',
locker: 'vn-worker-summary vn-one:nth-child(1) > vn-label-value:nth-child(10) > section > span',
userId: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(2) > section > span', userId: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(2) > section > span',
userName: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(3) > section > span', userName: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(3) > section > span',
role: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(4) > section > span', role: 'vn-worker-summary vn-one:nth-child(2) > vn-label-value:nth-child(4) > section > span',
@ -983,6 +985,7 @@ export default {
name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]', name: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.firstName"]',
surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]', surname: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.lastName"]',
phone: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.phone"]', phone: 'vn-worker-basic-data vn-textfield[ng-model="$ctrl.worker.phone"]',
locker: 'vn-worker-basic-data vn-input-number[ng-model="$ctrl.worker.locker"]',
saveButton: 'vn-worker-basic-data button[type=submit]' saveButton: 'vn-worker-basic-data button[type=submit]'
}, },
workerPbx: { workerPbx: {
@ -1040,6 +1043,12 @@ export default {
switft: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bankEntityFk"]', switft: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bankEntityFk"]',
createButton: 'vn-worker-create vn-submit[label="Create"]', createButton: 'vn-worker-create vn-submit[label="Create"]',
}, },
workerPda: {
currentPDA: 'vn-worker-pda vn-textfield[ng-model="$ctrl.currentPDA.description"]',
newPDA: 'vn-worker-pda vn-autocomplete[ng-model="$ctrl.newPDA"]',
delete: 'vn-worker-pda vn-icon-button[icon=delete]',
submit: 'vn-worker-pda vn-submit[label="Assign"]',
},
invoiceOutIndex: { invoiceOutIndex: {
topbarSearch: 'vn-searchbar', topbarSearch: 'vn-searchbar',
searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr', searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr',

View File

@ -29,5 +29,6 @@ describe('Worker summary path', () => {
expect(await page.getProperty(selectors.workerSummary.userName, 'innerText')).toEqual('agency'); expect(await page.getProperty(selectors.workerSummary.userName, 'innerText')).toEqual('agency');
expect(await page.getProperty(selectors.workerSummary.role, 'innerText')).toEqual('agency'); expect(await page.getProperty(selectors.workerSummary.role, 'innerText')).toEqual('agency');
expect(await page.getProperty(selectors.workerSummary.extension, 'innerText')).toEqual('1101'); expect(await page.getProperty(selectors.workerSummary.extension, 'innerText')).toEqual('1101');
expect(await page.getProperty(selectors.workerSummary.locker, 'innerText')).toEqual('-');
}); });
}); });

View File

@ -25,6 +25,7 @@ describe('Worker basic data path', () => {
await page.overwrite(selectors.workerBasicData.name, 'David C.'); await page.overwrite(selectors.workerBasicData.name, 'David C.');
await page.overwrite(selectors.workerBasicData.surname, 'H.'); await page.overwrite(selectors.workerBasicData.surname, 'H.');
await page.overwrite(selectors.workerBasicData.phone, '444332211'); await page.overwrite(selectors.workerBasicData.phone, '444332211');
await page.overwrite(selectors.workerBasicData.locker, '1');
await page.click(selectors.workerBasicData.saveButton); await page.click(selectors.workerBasicData.saveButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -36,5 +37,6 @@ describe('Worker basic data path', () => {
expect(await page.waitToGetProperty(selectors.workerBasicData.name, 'value')).toEqual('David C.'); expect(await page.waitToGetProperty(selectors.workerBasicData.name, 'value')).toEqual('David C.');
expect(await page.waitToGetProperty(selectors.workerBasicData.surname, 'value')).toEqual('H.'); expect(await page.waitToGetProperty(selectors.workerBasicData.surname, 'value')).toEqual('H.');
expect(await page.waitToGetProperty(selectors.workerBasicData.phone, 'value')).toEqual('444332211'); expect(await page.waitToGetProperty(selectors.workerBasicData.phone, 'value')).toEqual('444332211');
expect(await page.waitToGetProperty(selectors.workerBasicData.locker, 'value')).toEqual('1');
}); });
}); });

View File

@ -0,0 +1,41 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Worker pda path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('hr', 'worker');
await page.accessToSearchResult('employeeNick');
await page.accessToSection('worker.card.pda');
});
afterAll(async() => {
await browser.close();
});
it('should check if worker has already a PDA allocated', async() => {
expect(await page.waitToGetProperty(selectors.workerPda.currentPDA, 'value')).toContain('serialNumber1');
});
it('should deallocate the PDA', async() => {
await page.waitToClick(selectors.workerPda.delete);
let message = await page.waitForSnackbar();
expect(message.text).toContain('PDA deallocated');
});
it('should allocate a new PDA', async() => {
await page.autocompleteSearch(selectors.workerPda.newPDA, 'serialNumber2');
await page.waitToClick(selectors.workerPda.submit);
let message = await page.waitForSnackbar();
expect(message.text).toContain('PDA allocated');
});
it('should check if a new PDA has been allocated', async() => {
expect(await page.waitToGetProperty(selectors.workerPda.currentPDA, 'value')).toContain('serialNumber2');
});
});

View File

@ -227,9 +227,21 @@ describe('Ticket Edit sale path', () => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund); await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale'); await page.waitForState('ticket.card.sale');
}); });
it('should show error trying to delete a ticket with a refund', async() => {
await page.accessToSearchResult('16');
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket);
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets with associated refunds can\'t be deleted');
await page.waitToClick(selectors.globalItems.cancelButton);
});
it('should select the third sale and create a claim of it', async() => { it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16'); await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale'); await page.accessToSection('ticket.card.sale');

View File

@ -19,6 +19,10 @@
padding-right: 0; padding-right: 0;
} }
} }
& > .icons.pre {
min-width: 22px
}
} }
&.readonly > .container > .icons.post { &.readonly > .container > .icons.post {
display: none; display: none;

View File

@ -58,6 +58,29 @@ vn-chip {
background-color: $color-font-bg-dark; background-color: $color-font-bg-dark;
color: $color-font-bg; color: $color-font-bg;
} }
&.pink,
&.pink.clickable:hover,
&.pink.clickable:focus {
background-color: $color-pink;
}
&.dark-notice,
&.dark-notice.clickable:hover,
&.dark-notice.clickable:focus {
background-color: $color-notice;
}
&.yellow,
&.yellow.clickable:hover,
&.yellow.clickable:focus {
background-color: $color-yellow;
}
&.none,
&.none.clickable:hover,
&.none.clickable:focus {
background-color: $color-bg-panel;
border-radius: 50%;
border: 1px solid $color-font-link
}
&.clickable { &.clickable {
@extend %clickable; @extend %clickable;
opacity: 0.8; opacity: 0.8;

View File

@ -12,7 +12,7 @@
<span class="required">*</span> <span class="required">*</span>
</label> </label>
</div> </div>
<div class="icons pre"> <div class="icons pre" ng-class="{'clearable': $ctrl.clearDisabled != true}">
<vn-icon ng-show="::$ctrl.clearDisabled != true" <vn-icon ng-show="::$ctrl.clearDisabled != true"
icon="clear" icon="clear"
translate-attr="{title: 'Clear'}" translate-attr="{title: 'Clear'}"

View File

@ -151,6 +151,9 @@
display: none; display: none;
} }
} }
& > .icons.pre.clearable {
min-width: 22px
}
& > .underline { & > .underline {
position: absolute; position: absolute;
bottom: 0; bottom: 0;

View File

@ -23,6 +23,7 @@ There is a new version, click here to reload: Hay una nueva versión, pulse aqu
Back: Volver Back: Volver
Save: Guardar Save: Guardar
Assign: Asignar
Create: Crear Create: Crear
Send: Enviar Send: Enviar
Delete: Eliminar Delete: Eliminar

View File

@ -147,8 +147,8 @@
"Receipt's bank was not found": "Receipt's bank was not found", "Receipt's bank was not found": "Receipt's bank was not found",
"This receipt was not compensated": "This receipt was not compensated", "This receipt was not compensated": "This receipt was not compensated",
"Client's email was not found": "Client's email was not found", "Client's email was not found": "Client's email was not found",
"Tickets with associated refunds": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº {{id}}",
"It is not possible to modify tracked sales": "It is not possible to modify tracked sales", "It is not possible to modify tracked sales": "It is not possible to modify tracked sales",
"It is not possible to modify sales that their articles are from Floramondo": "It is not possible to modify sales that their articles are from Floramondo", "It is not possible to modify sales that their articles are from Floramondo": "It is not possible to modify sales that their articles are from Floramondo",
"It is not possible to modify cloned sales": "It is not possible to modify cloned sales", "It is not possible to modify cloned sales": "It is not possible to modify cloned sales"
"Valid priorities: 1,2,3": "Valid priorities: 1,2,3"
} }

View File

@ -263,6 +263,8 @@
"It is not possible to modify sales that their articles are from Floramondo": "No es posible modificar líneas de pedido cuyos artículos sean de Floramondo", "It is not possible to modify sales that their articles are from Floramondo": "No es posible modificar líneas de pedido cuyos artículos sean de Floramondo",
"It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas", "It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas",
"A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.", "A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.",
"There is no assigned email for this client": "No hay correo asignado para este cliente" "There is no assigned email for this client": "No hay correo asignado para este cliente",
"This locker has already been assigned": "Esta taquilla ya ha sido asignada",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Not exist this branch": "La rama no existe"
} }

View File

@ -57,6 +57,7 @@ module.exports = Self => {
const itemType = await models.ItemType.findById(params.typeFk, {fields: ['isLaid']}, myOptions); const itemType = await models.ItemType.findById(params.typeFk, {fields: ['isLaid']}, myOptions);
params.isLaid = itemType.isLaid; params.isLaid = itemType.isLaid;
params.isPhotoRequested = true;
const item = await models.Item.create(params, myOptions); const item = await models.Item.create(params, myOptions);

View File

@ -146,6 +146,12 @@
}, },
"isLaid": { "isLaid": {
"type": "boolean" "type": "boolean"
},
"isPhotoRequested": {
"type": "boolean",
"mysql":{
"columnName": "doPhoto"
}
} }
}, },
"relations": { "relations": {

View File

@ -182,6 +182,12 @@
ng-model="$ctrl.item.isFragile" ng-model="$ctrl.item.isFragile"
info="Is shown at website, app that this item cannot travel (wreath, palms, ...)"> info="Is shown at website, app that this item cannot travel (wreath, palms, ...)">
</vn-check> </vn-check>
<vn-check
vn-one
label="Do photo"
ng-model="$ctrl.item.isPhotoRequested"
info="This item does need a photo">
</vn-check>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -12,3 +12,5 @@ Fragile: Frágil
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...) Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Multiplier: Multiplicador Multiplier: Multiplicador
Generic: Genérico Generic: Genérico
This item does need a photo: Este artículo necesita una foto
Do photo: Hacer foto

View File

@ -91,6 +91,9 @@ class Controller extends Descriptor {
this.$.photo.setAttribute('src', newSrc); this.$.photo.setAttribute('src', newSrc);
this.$.photo.setAttribute('zoom-image', newZoomSrc); this.$.photo.setAttribute('zoom-image', newZoomSrc);
if (this.item.isPhotoRequested)
this.$http.patch(`Items/${this.item.id}`, {isPhotoRequested: false});
} }
getWarehouseName(warehouseFk) { getWarehouseName(warehouseFk) {

View File

@ -8,7 +8,8 @@ describe('vnItemDescriptor', () => {
id: 1, id: 1,
itemType: { itemType: {
warehouseFk: 1 warehouseFk: 1
} },
isPhotoRequested: true
}; };
const stock = { const stock = {
visible: 1, visible: 1,
@ -43,4 +44,16 @@ describe('vnItemDescriptor', () => {
expect(controller.item).toEqual(item); expect(controller.item).toEqual(item);
}); });
}); });
describe('onUploadResponse()', () => {
it(`should change isPhotoRequested when a new photo is uploaded`, () => {
controller.item = item;
controller.$rootScope = {imagePath: () => {}};
controller.$.photo = {setAttribute: () => {}};
$httpBackend.expectPATCH(`Items/${item.id}`).respond(200);
controller.onUploadResponse();
$httpBackend.flush();
});
});
}); });

View File

@ -70,6 +70,7 @@ class Controller extends Section {
} }
$onDestroy() { $onDestroy() {
if (this.$state.getCurrentPath()[2].state.name === 'item')
this.card.reload(); this.card.reload();
} }
} }

View File

@ -75,6 +75,15 @@
{{$ctrl.summary.item.itemType.worker.user.name}} {{$ctrl.summary.item.itemType.worker.user.name}}
</span> </span>
</vn-label-value> </vn-label-value>
<vn-horizontal>
<vn-check
label="Do photo"
disabled="true"
vn-one
ng-model="$ctrl.item.isPhotoRequested"
info="This item does need a photo">
</vn-check>
</vn-horizontal>
</vn-one> </vn-one>
<vn-one name="otherData"> <vn-one name="otherData">
<h4 ng-show="$ctrl.isBuyer"> <h4 ng-show="$ctrl.isBuyer">

View File

@ -33,16 +33,16 @@
</vn-icon-button> </vn-icon-button>
</vn-td> </vn-td>
</vn-tr> </vn-tr>
<vn-tr ng-repeat="waste in detail.lines" class="clickable vn-tr" <a ng-repeat="waste in detail.lines" class="clickable vn-tr"
ui-sref="item.waste.detail({buyer: waste.buyer, family: waste.family})" ui-sref="item.waste.detail({buyer: waste.buyer, family: waste.family})"
ng-class="{'hidden': !$ctrl.wasteConfig[detail.buyer].hidden}"> ng-show="$ctrl.wasteConfig[detail.buyer].hidden">
<vn-td></vn-td> <vn-td></vn-td>
<vn-td>{{::waste.family}}</vn-td> <vn-td>{{::waste.family}}</vn-td>
<vn-td number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td> <vn-td number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
<vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td> <vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>
<vn-td number>{{::waste.total | currency: 'EUR'}}</vn-td> <vn-td number>{{::waste.total | currency: 'EUR'}}</vn-td>
<vn-td shrink></vn-td> <vn-td shrink></vn-td>
</vn-tr> </a>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>
</vn-card> </vn-card>

View File

@ -47,7 +47,16 @@ module.exports = Self => {
verb: 'POST' verb: 'POST'
} }
}); });
Self.upload = async(ctx, options) => { Self.upload = async(
ctx,
appName,
toVersion,
branch,
fromVersion,
description,
unlock,
options
) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
@ -55,12 +64,6 @@ module.exports = Self => {
const AccessContainer = models.AccessContainer; const AccessContainer = models.AccessContainer;
const fileOptions = {}; const fileOptions = {};
let tx; let tx;
const appName = ctx.args.appName;
const toVersion = ctx.args.toVersion;
const branch = ctx.args.branch;
const fromVersion = ctx.args.fromVersion;
let description = ctx.args.description;
const unlock = ctx.args.unlock;
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
@ -153,11 +156,14 @@ module.exports = Self => {
formatDesc += `*${appName.toUpperCase()}* v.${toVersion} `; formatDesc += `*${appName.toUpperCase()}* v.${toVersion} `;
const oldVersion = await models.MdbVersionTree.findOne({ const oldVersion = await models.MdbVersionTree.findOne({
where: {version: fromVersion}, where: {
version: fromVersion,
app: appName
},
fields: ['branchFk'] fields: ['branchFk']
}, myOptions); }, myOptions);
if (branch == oldVersion.branchFk) if (!oldVersion || branch == oldVersion.branchFk)
formatDesc += `[*${branch}*]: `; formatDesc += `[*${branch}*]: `;
else else
formatDesc += `[*${oldVersion.branchFk}* » *${branch}*]: `; formatDesc += `[*${oldVersion.branchFk}* » *${branch}*]: `;

View File

@ -93,7 +93,14 @@
<vn-tr ng-repeat="ticket in $ctrl.summary.tickets"> <vn-tr ng-repeat="ticket in $ctrl.summary.tickets">
<vn-td shrink>{{ticket.priority | dashIfEmpty}}</vn-td> <vn-td shrink>{{ticket.priority | dashIfEmpty}}</vn-td>
<vn-td expand title="{{ticket.address.street}}">{{ticket.street}}</vn-td> <vn-td expand title="{{ticket.address.street}}">{{ticket.street}}</vn-td>
<vn-td expand>{{ticket.city}}</vn-td> <vn-td
expand
ng-click="$ctrl.goToBuscaman(ticket)"
class="link"
vn-tooltip="Open buscaman"
tooltip-position="up">
{{::ticket.city}}
</vn-td>
<vn-td shrink>{{ticket.postalCode}}</vn-td> <vn-td shrink>{{ticket.postalCode}}</vn-td>
<vn-td> <vn-td>
<span <span

View File

@ -31,6 +31,21 @@ class Controller extends Summary {
this.sumPackages(); this.sumPackages();
}); });
} }
goToBuscaman(ticket) {
if (!this.route.vehicleFk)
throw new UserError(`The route doesn't have a vehicle`);
let query = `Routes/${this.route.vehicleFk}/getDeliveryPoint`;
this.$http.get(query).then(response => {
if (!response.data)
throw new UserError(`The route's vehicle doesn't have a delivery point`);
const address = response.data + '+to:' + ticket.postalCode + ' ' + ticket.city + ' ' + ticket.street;
const url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
window.open(url + encodeURI(address), '_blank');
});
}
} }
ngModule.vnComponent('vnRouteSummary', { ngModule.vnComponent('vnRouteSummary', {

View File

@ -37,5 +37,29 @@ describe('Route', () => {
expect(controller.packagesTotal).toEqual(4); expect(controller.packagesTotal).toEqual(4);
}); });
}); });
describe('goToBuscaman()', () => {
it('should open buscaman with the given arguments', () => {
jest.spyOn(window, 'open').mockReturnThis();
const expectedUrl = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=46460%20Av%20Espioca%20100+to:n19%20London%20my%20street';
controller.route = {vehicleFk: 1};
const url = `Routes/${controller.route.vehicleFk}/getDeliveryPoint`;
$httpBackend.when('GET', `Routes/1/summary`).respond();
$httpBackend.expectGET(url).respond('46460 Av Espioca 100');
const ticket = {
id: 1,
checked: true,
street: 'my street',
postalCode: 'n19',
city: 'London'
};
controller.goToBuscaman(ticket);
$httpBackend.flush();
expect(window.open).toHaveBeenCalledWith(expectedUrl, '_blank');
});
});
}); });
}); });

View File

@ -93,7 +93,14 @@
</vn-input-number> </vn-input-number>
</vn-td> </vn-td>
<vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td> <vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td>
<vn-td expand>{{::ticket.city}}</vn-td> <vn-td
expand
ng-click="$ctrl.goToBuscaman(ticket)"
class="link"
vn-tooltip="Open buscaman"
tooltip-position="up">
{{::ticket.city}}
</vn-td>
<vn-td shrink>{{::ticket.postalCode}}</vn-td> <vn-td shrink>{{::ticket.postalCode}}</vn-td>
<vn-td expand> <vn-td expand>
<span <span

View File

@ -74,29 +74,24 @@ class Controller extends Section {
return selectedItems; return selectedItems;
} }
goToBuscaman() { goToBuscaman(ticket) {
if (!this.route.vehicleFk) if (!this.route.vehicleFk)
throw new UserError(`The route doesn't have a vehicle`); throw new UserError(`The route doesn't have a vehicle`);
let query = `Routes/${this.route.vehicleFk}/getDeliveryPoint`;
this.$http.get(query).then(response => { this.$http.get(`Routes/${this.route.vehicleFk}/getDeliveryPoint`).then(res => {
if (!response.data) if (!res.data)
throw new UserError(`The route's vehicle doesn't have a delivery point`); throw new UserError(`The route's vehicle doesn't have a delivery point`);
return response.data; let addresses = res.data;
}).then(address => { const lines = ticket ? [ticket] : this.getSelectedItems(this.tickets);
let addresses;
if (address) addresses = address;
let lines = this.getSelectedItems(this.tickets);
let url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
lines.forEach((line, index) => { lines.forEach((line, index) => {
const previusLine = lines[index - 1] ? lines[index - 1].street : null; const previousLine = lines[index - 1] ? lines[index - 1].street : null;
if (previusLine != line.street) if (previousLine != line.street)
addresses = addresses + '+to:' + line.postalCode + ' ' + line.city + ' ' + line.street; addresses = addresses + '+to:' + line.postalCode + ' ' + line.city + ' ' + line.street;
}); });
window.open(url + addresses, '_blank'); const url = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=';
window.open(url + encodeURI(addresses), '_blank');
}); });
} }

View File

@ -136,7 +136,7 @@ describe('Route', () => {
describe('goToBuscaman()', () => { describe('goToBuscaman()', () => {
it('should open buscaman with the given arguments', () => { it('should open buscaman with the given arguments', () => {
jest.spyOn(window, 'open').mockReturnThis(); jest.spyOn(window, 'open').mockReturnThis();
const expectedUrl = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=46460 Av Espioca 100+to:n19 London my street'; const expectedUrl = 'http://gps.buscalia.com/usuario/localizar.aspx?bmi=true&addr=46460%20Av%20Espioca%20100+to:n19%20London%20my%20street';
controller.route = {vehicleFk: 1}; controller.route = {vehicleFk: 1};
const url = `Routes/${controller.route.vehicleFk}/getDeliveryPoint`; const url = `Routes/${controller.route.vehicleFk}/getDeliveryPoint`;
$httpBackend.expectGET(url).respond('46460 Av Espioca 100'); $httpBackend.expectGET(url).respond('46460 Av Espioca 100');

View File

@ -64,6 +64,14 @@ module.exports = Self => {
{ {
arg: 'healthRegister', arg: 'healthRegister',
type: 'string' type: 'string'
},
{
arg: 'isVies',
type: 'boolean'
},
{
arg: 'isTrucker',
type: 'boolean'
}], }],
returns: { returns: {
arg: 'res', arg: 'res',

View File

@ -110,6 +110,9 @@
}, },
"healthRegister": { "healthRegister": {
"type": "string" "type": "string"
},
"isVies": {
"type": "boolean"
} }
}, },
"relations": { "relations": {

View File

@ -172,7 +172,7 @@
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-id="country" vn-one <vn-autocomplete vn-id="country" vn-two
ng-model="$ctrl.supplier.countryFk" ng-model="$ctrl.supplier.countryFk"
data="countries" data="countries"
show-field="country" show-field="country"
@ -180,10 +180,17 @@
label="Country" label="Country"
rule> rule>
</vn-autocomplete> </vn-autocomplete>
<vn-two class="vn-pl-xs">
<vn-check <vn-check
label="Trucker" label="Trucker"
ng-model="$ctrl.supplier.isTrucker"> ng-model="$ctrl.supplier.isTrucker">
</vn-check> </vn-check>
<vn-check
class="vn-ml-lg"
label="Vies"
ng-model="$ctrl.supplier.isVies">
</vn-check>
</vn-two>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -111,7 +111,7 @@ module.exports = Self => {
args.landed = landedResult && landedResult.landed; args.landed = landedResult && landedResult.landed;
} }
query = `CALL vn.ticketCreateWithUser(?, ?, ?, ?, ?, ?, ?, ?, ?, @result); query = `CALL vn.ticket_add(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, @result);
SELECT @result newTicketId;`; SELECT @result newTicketId;`;
const result = await Self.rawSql(query, [ const result = await Self.rawSql(query, [
args.clientId, args.clientId,
@ -122,6 +122,7 @@ module.exports = Self => {
args.agencyModeId || null, args.agencyModeId || null,
args.routeId || null, args.routeId || null,
args.landed, args.landed,
true,
myUserId myUserId
], myOptions); ], myOptions);

View File

@ -42,6 +42,14 @@ module.exports = Self => {
if (!isEditable) if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
// Check if ticket has refunds
const ticketRefunds = await models.TicketRefund.find({
where: {originalTicketFk: id},
fields: ['id']}
, myOptions);
if (ticketRefunds.length > 0)
throw new UserError($t('Tickets with associated refunds', {id: ticketRefunds[0].id}));
// Check if has sales with shelving // Check if has sales with shelving
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
const sales = await models.Sale.find({ const sales = await models.Sale.find({

View File

@ -101,4 +101,32 @@ describe('ticket setDeleted()', () => {
throw e; throw e;
} }
}); });
it('should show error trying to delete a ticket with a refund', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost:5000'},
}
};
ctx.req.__ = value => {
return value;
};
const ticketId = 12;
await models.Ticket.setDeleted(ctx, ticketId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toContain('Tickets with associated refunds');
});
}); });

View File

@ -80,34 +80,6 @@ describe('sale transferSales()', () => {
expect(error.message).toEqual(`Can't transfer claimed sales`); expect(error.message).toEqual(`Can't transfer claimed sales`);
}); });
it('should be able to transfer claimed sales if the role is claimManager', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const claimManagerId = 72;
const myActiveCtx = {
accessToken: {userId: claimManagerId},
};
const myCtx = {req: myActiveCtx};
const ticketId = 11;
const receiverTicketId = null;
const sales = await models.Ticket.getSales(ticketId, options);
await models.Ticket.transferSales(myCtx, ticketId, receiverTicketId, sales, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should transfer the sales from a ticket to a new one', async() => { it('should transfer the sales from a ticket to a new one', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});

View File

@ -77,9 +77,8 @@ module.exports = Self => {
const saleIds = sales.map(sale => sale.id); const saleIds = sales.map(sale => sale.id);
const isClaimManager = await models.Account.hasRole(userId, 'claimManager');
const hasClaimedSales = await models.ClaimBeginning.findOne({where: {saleFk: {inq: saleIds}}}); const hasClaimedSales = await models.ClaimBeginning.findOne({where: {saleFk: {inq: saleIds}}});
if (hasClaimedSales && !isClaimManager) if (hasClaimedSales)
throw new UserError(`Can't transfer claimed sales`); throw new UserError(`Can't transfer claimed sales`);
for (const sale of sales) { for (const sale of sales) {

View File

@ -24,11 +24,43 @@
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="sale in $ctrl.sales"> <vn-tr ng-repeat="sale in $ctrl.sales">
<vn-td center> <vn-td center>
<span class="chip {{$ctrl.chipHasSaleGroupDetail(sale.preparingList.hasSaleGroupDetail)}} vn-mx-xs chip2" vn-tooltip="has saleGroupDetail"></span> <vn-chip
<span class="chip {{$ctrl.chipIsPreviousSelected(sale.preparingList.isPreviousSelected)}} vn-ml-xs" vn-tooltip="is previousSelected"></span> ng-class="::{
<span class="chip {{$ctrl.chipIsPrevious(sale.preparingList.isPrevious)}} vn-mr-xs" vn-tooltip="is previous"></span> 'pink': sale.preparingList.hasSaleGroupDetail,
<span class="chip {{$ctrl.chipIsPrepared(sale.preparingList.isPrepared)}} vn-mx-xs" vn-tooltip="is prepared"></span> 'none': !sale.preparingList.hasSaleGroupDetail,
<span class="chip {{$ctrl.chipIsControled(sale.preparingList.isControled)}} vn-mx-xs" vn-tooltip="is controled"></span> }"
class="circle"
vn-tooltip="has saleGroupDetail"
>
</vn-chip>
<vn-chip ng-class="::{
'notice': sale.preparingList.isPreviousSelected,
'none': !sale.preparingList.isPreviousSelected,
}"
class="circle"
vn-tooltip="is previousSelected">
</vn-chip>
<vn-chip ng-class="::{
'dark-notice': sale.preparingList.isPrevious,
'none': !sale.preparingList.isPrevious,
}"
class="circle"
vn-tooltip="is previous">
</vn-chip>
<vn-chip ng-class="::{
'warning': sale.preparingList.isPrepared,
'none': !sale.preparingList.isPrepared,
}"
class="circle"
vn-tooltip="is prepared">
</vn-chip>
<vn-chip ng-class="::{
'yellow': sale.preparingList.isControled,
'none': !sale.preparingList.isControled,
}"
class="circle"
vn-tooltip="is controled">
</vn-chip>
</vn-td> </vn-td>
<vn-td number> <vn-td number>
<span <span

View File

@ -74,31 +74,6 @@ class Controller extends Section {
this.$.itemDescriptor.show(event.target, sale.item.id); this.$.itemDescriptor.show(event.target, sale.item.id);
} }
chipHasSaleGroupDetail(hasSaleGroupDetail) {
if (hasSaleGroupDetail) return 'pink';
else return 'message';
}
chipIsPreviousSelected(isPreviousSelected) {
if (isPreviousSelected) return 'notice';
else return 'message';
}
chipIsPrevious(isPrevious) {
if (isPrevious) return 'dark-notice';
else return 'message';
}
chipIsPrepared(isPrepared) {
if (isPrepared) return 'warning';
else return 'message';
}
chipIsControled(isControled) {
if (isControled) return 'yellow';
else return 'message';
}
showSaleTracking(sale) { showSaleTracking(sale) {
this.saleId = sale.id; this.saleId = sale.id;
this.$.saleTracking.show(); this.$.saleTracking.show();

View File

@ -1,7 +1,12 @@
@import "variables"; @import "variables";
.chip { .circle {
display: inline-block; display: inline-block;
min-width: 15px; justify-content: center;
min-height: 25px; align-items: center;
border-radius: 50%;
font-size: .9rem;
width: 30px;
height: 30px;
cursor: pointer;
} }

View File

@ -0,0 +1,66 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('allocatePDA', {
description: 'Deallocate the PDA of the worker',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
}, {
arg: 'pda',
type: 'number',
required: true,
description: 'The pda id'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/allocatePDA`,
verb: 'POST'
}
});
Self.allocatePDA = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const pda = await models.DeviceProduction.findById(args.pda, myOptions);
if (pda.stateFk != 'idle') throw new UserError(`The PDA state is not idle`);
await pda.updateAttributes({stateFk: 'active'}, myOptions);
await models.DeviceProductionUser.create({
deviceProductionFk: args.pda,
userFk: args.id,
created: new Date()
}, myOptions);
if (tx) await tx.commit();
return {
deviceProductionFk: pda.id,
deviceProduction: {
modelFk: pda.modelFk,
serialNumber: pda.serialNumber
}
};
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,53 @@
module.exports = Self => {
Self.remoteMethodCtx('deallocatePDA', {
description: 'Deallocate the worker\'s PDA',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
}, {
arg: 'pda',
type: 'number',
required: true,
description: 'The pda id'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/deallocatePDA`,
verb: 'POST'
}
});
Self.deallocatePDA = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const pda = await models.DeviceProduction.findById(args.pda, myOptions);
await pda.updateAttributes({stateFk: 'idle'}, myOptions);
await models.DeviceProductionUser.destroyAll({userFk: args.id, deviceProductionFk: args.pda}, myOptions);
if (tx) await tx.commit();
return pda;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,46 @@
const models = require('vn-loopback/server/server').models;
describe('Worker allocatePDA()', () => {
it('should allocate a new worker\'s PDA', async() => {
const tx = await models.Worker.beginTransaction({});
try {
const workerWithoutPDA = 1101;
const PDAWithIdleState = 4;
const options = {transaction: tx};
const ctx = {args: {id: workerWithoutPDA, pda: PDAWithIdleState}};
await models.Worker.allocatePDA(ctx, options);
const deviceProduction = await models.DeviceProduction.findById(PDAWithIdleState, null, options);
const deviceProductionUser = await models.DeviceProductionUser
.findById(PDAWithIdleState, null, options);
expect(deviceProduction.stateFk).toEqual('active');
expect(deviceProductionUser).not.toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw error trying to allocate a non idle PDA', async() => {
const tx = await models.Worker.beginTransaction({});
let error;
try {
const workerWithoutPDA = 1101;
const PDAWithActiveState = 1;
const options = {transaction: tx};
const ctx = {args: {id: workerWithoutPDA, pda: PDAWithActiveState}};
await models.Worker.allocatePDA(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,26 @@
const models = require('vn-loopback/server/server').models;
describe('Worker deallocatePDA()', () => {
it('should deallocate a worker\'s PDA', async() => {
const tx = await models.Worker.beginTransaction({});
try {
const workerWithPDA = 1;
const PDAWithActiveState = 1;
const options = {transaction: tx};
const ctx = {args: {id: workerWithPDA, pda: PDAWithActiveState}};
await models.Worker.deallocatePDA(ctx, options);
const deviceProduction = await models.DeviceProduction.findById(PDAWithActiveState, null, options);
const deviceProductionUser = await models.DeviceProductionUser
.findById(PDAWithActiveState, null, options);
expect(deviceProduction.stateFk).toEqual('idle');
expect(deviceProductionUser).toBe(null);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -20,6 +20,18 @@
"Device": { "Device": {
"dataSource": "vn" "dataSource": "vn"
}, },
"DeviceProduction": {
"dataSource": "vn"
},
"DeviceProductionModels": {
"dataSource": "vn"
},
"DeviceProductionState": {
"dataSource": "vn"
},
"DeviceProductionUser": {
"dataSource": "vn"
},
"EducationLevel": { "EducationLevel": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,15 @@
{
"name": "DeviceProductionModels",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionModels"
}
},
"properties": {
"code": {
"type": "string",
"id": true
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "DeviceProductionState",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionState"
}
},
"properties": {
"code": {
"type": "string",
"id": true
},
"description": {
"type": "string"
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "DeviceProductionUser",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionUser"
}
},
"properties": {
"deviceProductionFk": {
"type": "number",
"id": true
},
"userFk": {
"type": "number"
},
"created": {
"type": "date"
}
},
"relations": {
"deviceProduction": {
"type": "belongsTo",
"model": "DeviceProduction",
"foreignKey": "deviceProductionFk"
},
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": "userFk"
}
}
}

View File

@ -0,0 +1,54 @@
{
"name": "DeviceProduction",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProduction"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"imei": {
"type": "string"
},
"modelFk": {
"type": "number"
},
"macWifi": {
"type" : "string"
},
"serialNumber": {
"type" : "string"
},
"android_id": {
"type" : "string"
},
"purchased": {
"type" : "date"
},
"stateFk": {
"type" : "string"
},
"isInScaleFusion": {
"type" : "boolean"
},
"description": {
"type" : "string"
}
},
"relations": {
"model": {
"type": "belongsTo",
"model": "DeviceProductionModels",
"foreignKey": "modelFk"
},
"state": {
"type": "belongsTo",
"model": "DeviceProductionState",
"foreignKey": "stateFk"
}
}
}

View File

@ -14,4 +14,10 @@ module.exports = Self => {
require('../methods/worker/holidays')(Self); require('../methods/worker/holidays')(Self);
require('../methods/worker/activeContract')(Self); require('../methods/worker/activeContract')(Self);
require('../methods/worker/new')(Self); require('../methods/worker/new')(Self);
require('../methods/worker/deallocatePDA')(Self);
require('../methods/worker/allocatePDA')(Self);
Self.validatesUniquenessOf('locker', {
message: 'This locker has already been assigned'
});
}; };

View File

@ -55,6 +55,9 @@
}, },
"code": { "code": {
"type" : "string" "type" : "string"
},
"locker": {
"type" : "number"
} }
}, },
"relations": { "relations": {

View File

@ -78,6 +78,11 @@
ng-model="$ctrl.worker.SSN" ng-model="$ctrl.worker.SSN"
rule> rule>
</vn-textfield> </vn-textfield>
<vn-input-number
min="0"
label="Locker"
ng-model="$ctrl.worker.locker">
</vn-input-number>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

@ -6,3 +6,4 @@ Married: Casado/a
Single: Soltero/a Single: Soltero/a
Business phone: Teléfono de empresa Business phone: Teléfono de empresa
Mobile extension: Extensión móvil Mobile extension: Extensión móvil
Locker: Taquilla

View File

@ -10,6 +10,7 @@ import './descriptor-popover';
import './search-panel'; import './search-panel';
import './basic-data'; import './basic-data';
import './pbx'; import './pbx';
import './pda';
import './department'; import './department';
import './calendar'; import './calendar';
import './time-control'; import './time-control';

View File

@ -24,3 +24,10 @@ Go to the worker: Ir al trabajador
Click to exclude the user from getting disabled: Marcar para no deshabilitar Click to exclude the user from getting disabled: Marcar para no deshabilitar
Click to allow the user to be disabled: Marcar para deshabilitar Click to allow the user to be disabled: Marcar para deshabilitar
This user can't be disabled: Fijado para no deshabilitar This user can't be disabled: Fijado para no deshabilitar
Model: Modelo
Serial Number: Número de serie
Current PDA: PDA Actual
Deallocate PDA: Desasignar PDA
PDA deallocated: PDA desasignada
PDA allocated: PDA asignada
New PDA: Nueva PDA

View File

@ -0,0 +1,49 @@
<div class="vn-w-md" ng-show="$ctrl.currentPDA">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Current PDA"
ng-model="$ctrl.currentPDA.description"
disabled="true">
<append>
<vn-icon-button
icon="delete"
vn-tooltip="Deallocate PDA"
ng-click="$ctrl.deallocatePDA()"
vn-acl="hr, productionAssi">
</vn-icon-button>
</append>
</vn-textfield>
</vn-horizontal>
</vn-card>
</div>
<form name="form" ng-show="!$ctrl.currentPDA" ng-submit="$ctrl.allocatePDA()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
vn-acl="hr, productionAssi"
ng-model="$ctrl.newPDA"
url="DeviceProductions"
fields="['id', 'modelFk', 'serialNumber']"
where="{'stateFk': 'idle'}"
label="New PDA"
order="id"
value-field="id"
show-field="serialNumber">
<tpl-item>
<span>ID: {{id}}</span>
<span class="separator"></span>
<span>{{'Model' | translate}}: {{modelFk}}</span>
<span class="separator"></span>
<span>{{'Serial Number' | translate}}: {{serialNumber}}</span>
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!$ctrl.newPDA"
label="Assign">
</vn-submit>
</vn-button-bar>
</form>

View File

@ -0,0 +1,53 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
const filter = {
where: {userFk: this.$params.id},
include: {relation: 'deviceProduction'}
};
this.$http.get('DeviceProductionUsers', {filter}).
then(res => {
if (res.data && res.data.length > 0)
this.setCurrentPDA(res.data[0]);
});
}
deallocatePDA() {
this.$http.post(`Workers/${this.$params.id}/deallocatePDA`, {pda: this.currentPDA.deviceProductionFk})
.then(() => {
this.vnApp.showSuccess(this.$t('PDA deallocated'));
delete this.currentPDA;
});
}
allocatePDA() {
this.$http.post(`Workers/${this.$params.id}/allocatePDA`, {pda: this.newPDA})
.then(res => {
if (res.data)
this.setCurrentPDA(res.data);
this.vnApp.showSuccess(this.$t('PDA allocated'));
delete this.newPDA;
});
}
setCurrentPDA(data) {
this.currentPDA = data;
this.currentPDA.description = [];
this.currentPDA.description.push(`ID: ${this.currentPDA.deviceProductionFk}`);
this.currentPDA.description.push(`${this.$t('Model')}: ${this.currentPDA.deviceProduction.modelFk}`);
this.currentPDA.description.push(`${this.$t('Serial Number')}: ${this.currentPDA.deviceProduction.serialNumber}`);
this.currentPDA.description = this.currentPDA.description.join(' ');
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnWorkerPda', {
template: require('./index.html'),
controller: Controller,
});

View File

@ -0,0 +1,72 @@
import './index';
describe('Worker', () => {
describe('Component vnWorkerPda', () => {
let $httpBackend;
let $scope;
let $element;
let controller;
beforeEach(ngModule('worker'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$element = angular.element('<vn-worker-pda></vn-worker-pda>');
controller = $componentController('vnWorkerPda', {$element, $scope});
$httpBackend.expectGET(`DeviceProductionUsers`).respond();
}));
describe('deallocatePDA()', () => {
it('should make an HTTP Post query to deallocatePDA', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.currentPDA = {deviceProductionFk: 1};
controller.$params.id = 1;
$httpBackend
.expectPOST(`Workers/${controller.$params.id}/deallocatePDA`,
{pda: controller.currentPDA.deviceProductionFk})
.respond();
controller.deallocatePDA();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.currentPDA).toBeUndefined();
});
});
describe('allocatePDA()', () => {
it('should make an HTTP Post query to allocatePDA', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.newPDA = 4;
controller.$params.id = 1;
$httpBackend
.expectPOST(`Workers/${controller.$params.id}/allocatePDA`,
{pda: controller.newPDA})
.respond();
controller.allocatePDA();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.newPDA).toBeUndefined();
});
});
describe('setCurrentPDA()', () => {
it('should set CurrentPDA', () => {
const data = {
deviceProductionFk: 1,
deviceProduction: {
modelFk: 1,
serialNumber: 1
}
};
controller.setCurrentPDA(data);
expect(controller.currentPDA).toBeDefined();
expect(controller.currentPDA.description).toBeDefined();
});
});
});
});

View File

@ -0,0 +1,6 @@
span.separator{
border-left: 1px solid black;
height: 100%;
margin: 0 10px;
}

View File

@ -11,9 +11,10 @@
], ],
"card": [ "card": [
{"state": "worker.card.basicData", "icon": "settings"}, {"state": "worker.card.basicData", "icon": "settings"},
{"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.pda", "icon": "phone_android"},
{"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.dms.index", "icon": "cloud_upload"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"},
{ {
"icon": "icon-wiki", "icon": "icon-wiki",
@ -141,6 +142,13 @@
"component": "vn-worker-create", "component": "vn-worker-create",
"description": "New worker", "description": "New worker",
"acl": ["hr"] "acl": ["hr"]
},
{
"url": "/pda",
"state": "worker.card.pda",
"component": "vn-worker-pda",
"description": "PDA",
"acl": ["hr", "productionAssi"]
} }
] ]
} }

View File

@ -47,6 +47,9 @@
<vn-label-value label="Personal phone" <vn-label-value label="Personal phone"
value="{{worker.client.phone}}"> value="{{worker.client.phone}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Locker"
value="{{worker.locker}}">
</vn-label-value>
</vn-one> </vn-one>
<vn-one> <vn-one>
<h4 translate>User data</h4> <h4 translate>User data</h4>

View File

@ -1,3 +1,4 @@
Business phone: Teléfono de empresa Business phone: Teléfono de empresa
Personal phone: Teléfono personal Personal phone: Teléfono personal
Mobile extension: Extensión móvil Mobile extension: Extensión móvil
Locker: Taquilla

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-back", "name": "salix-back",
"version": "23.08.01", "version": "23.10.01",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "Salix backend", "description": "Salix backend",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@ -66,8 +66,10 @@ module.exports = {
else else
value = '---'; value = '---';
if (labelData.routeFk) if (labelData.routeFk) {
value = `${value} [${labelData.routeFk.toString().substring(0, 3)}]`; let routeFk = labelData.routeFk.toString();
value = `${value} [${routeFk.substring(routeFk.length - 3)}]`;
}
return value; return value;
}, },

View File

@ -3,9 +3,9 @@
"height": "4.9cm", "height": "4.9cm",
"margin": { "margin": {
"top": "0.3cm", "top": "0.3cm",
"right": "0.6cm", "right": "0.3cm",
"bottom": "0cm", "bottom": "0cm",
"left": "0cm" "left": "0.2cm"
}, },
"printBackground": true "printBackground": true
} }