Merge branch 'dev' into 4797-lilium-worker-notifications

This commit is contained in:
Pau 2023-01-24 08:20:45 +01:00
commit dfa3022949
72 changed files with 1690 additions and 3770 deletions

View File

@ -5,20 +5,31 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2302.01] - 2023-01-12
## [2304.01] - 2023-02-09
### Added
- [General](Inicio) Permite recuperar la contraseña
- [Ticket](Opciones) Subir albarán a Docuware
- [Ticket](Opciones) Enviar correo con PDF de Docuware
- [Artículo](Datos Básicos) Añadido campo Unidades/Caja
- (Trabajadores -> Nuevo trabajador) Nueva sección
### Changed
- [Reclamaciones](Descriptor) Cambiado el campo Agencia por Zona
- [Tickets](Líneas preparadas) Actualizada sección para que sea más visual
-
### Fixed
- [General] Al utilizar el traductor de Google se descuadraban los iconos
-
## [2302.01] - 2023-01-26
### Added
- (General -> Inicio) Permite recuperar la contraseña
- (Tickets -> Opciones) Subir albarán a Docuware
- (Tickets -> Opciones) Enviar correo con PDF de Docuware
- (Artículos -> Datos Básicos) Añadido campo Unidades/Caja
### Changed
- (Reclamaciones -> Descriptor) Cambiado el campo Agencia por Zona
- (Tickets -> Líneas preparadas) Actualizada sección para que sea más visual
### Fixed
- (General) Al utilizar el traductor de Google se descuadraban los iconos
### Removed
- [Tickets](Control clientes) Eliminada sección
- (Tickets -> Control clientes) Eliminada sección

View File

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

View File

@ -36,19 +36,26 @@ module.exports = Self => {
JOIN osticket.ost_ticket_status ots ON ots.id = ot.status_id
JOIN osticket.ost_thread ot2 ON ot2.object_id = ot.ticket_id AND ot2.object_type = 'T'
JOIN (
SELECT ote.thread_id, MAX(ote.created) created, MAX(ote.updated) updated
SELECT sub2.thread_id, sub2.type, sub2.updated, sub2.created
FROM (
SELECT ote.thread_id, ote.created, ote.updated, ote.type
FROM osticket.ost_thread_entry ote
WHERE ote.staff_id AND ote.type = 'R'
GROUP BY ote.thread_id
WHERE ote.staff_id
ORDER BY ote.id DESC
LIMIT 10000000000000000000) sub2
GROUP BY sub2.thread_id
) sub ON sub.thread_id = ot2.id
WHERE ot.isanswered
AND ots.state = ?
AND IF(sub.updated > sub.created, sub.updated, sub.created) < DATE_SUB(CURDATE(), INTERVAL ? DAY)`;
AND ots.id IN (?)
AND sub.type = 'R'
AND IF(sub.updated > sub.created, sub.updated, sub.created) < DATE_SUB(CURDATE(), INTERVAL ? DAY);`;
const ticketsId = [];
const statusIdToClose = config.oldStatus.split(',');
con.connect(err => {
if (err) throw err;
con.query(sql, [config.oldStatus, config.day],
con.query(sql, [statusIdToClose, config.day],
(err, results) => {
if (err) throw err;
for (const result of results)

View File

@ -3,4 +3,5 @@ module.exports = Self => {
require('../methods/collection/newCollection')(Self);
require('../methods/collection/getSectors')(Self);
require('../methods/collection/setSaleQuantity')(Self);
require('../methods/collection/previousLabel')(Self);
};

View File

@ -0,0 +1,3 @@
UPDATE `vn`.`collection`
SET sectorFk=1
WHERE id=1;

View File

@ -0,0 +1,5 @@
UPDATE `vn`.`osTicketConfig`
SET oldStatus='1,6'
WHERE id=0;

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`itemPackingType` ADD isActive BOOLEAN NOT NULL;
UPDATE `vn`.`itemPackingType` SET isActive = 0 WHERE code IN ('P', 'F');
UPDATE `vn`.`itemPackingType` SET isActive = 1 WHERE code IN ('V', 'H');

View File

@ -0,0 +1,9 @@
ALTER TABLE `vn`.`itemConfig` ADD defaultTag INT DEFAULT 56 NOT NULL;
ALTER TABLE `vn`.`itemConfig` ADD CONSTRAINT itemConfig_FK FOREIGN KEY (defaultTag) REFERENCES vn.tag(id);
ALTER TABLE `vn`.`itemConfig` ADD validPriorities varchar(50) DEFAULT '[1,2,3]' NOT NULL;
ALTER TABLE `vn`.`itemConfig` ADD defaultPriority INT DEFAULT 2 NOT NULL;
ALTER TABLE `vn`.`item` MODIFY COLUMN relevancy tinyint(1) DEFAULT 0 NOT NULL COMMENT 'La web ordena de forma descendiente por este campo para mostrar los artículos';
INSERT INTO `salix`.`ACL`
(model, property, accessType, permission, principalType, principalId)
VALUES('ItemConfig', '*', 'READ', 'ALLOW', 'ROLE', 'buyer');

View File

@ -0,0 +1,6 @@
ALTER TABLE `vn`.`workerTimeControlConfig` ADD teleworkingStart INT NULL COMMENT 'Hora comienzo jornada de los teletrabajdores expresada en segundos';
ALTER TABLE `vn`.`workerTimeControlConfig` ADD teleworkingStartBreakTime INT NULL COMMENT 'Hora comienzo descanso de los teletrabjadores expresada en segundos';
UPDATE `vn`.`workerTimeControlConfig`
SET `teleworkingStart`=28800, `teleworkingStartBreakTime`=32400
WHERE `id`=1;

View File

@ -0,0 +1,24 @@
UPDATE `salix`.`ACL`
SET accessType='READ'
WHERE model='Worker'
AND property='*'
AND accessType='*'
AND permission='ALLOW'
AND principalType='ROLE'
AND principalId='employee';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Worker', 'updateAttributes', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('Worker', 'createAbsence', '*', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'updateAbsence', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'deleteAbsence', '*', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'new', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('Role', '*', 'READ', 'ALLOW', 'ROLE', 'hr');
ALTER TABLE `vn`.`workerConfig` ADD roleFk int(10) unsigned NOT NULL COMMENT 'Rol por defecto al dar de alta un trabajador nuevo';
UPDATE `vn`.`workerConfig`
SET roleFk = 1
WHERE id = 1;

View File

@ -1215,7 +1215,7 @@ INSERT INTO `vn`.`tag`(`id`, `code`, `name`, `isFree`, `isQuantitatif`, `sourceT
(7, NULL, 'Ancho de la base', 1, 1, NULL, 'mm',NULL, NULL),
(23, 'stems', 'Tallos', 1, 1, NULL, NULL, NULL, 'stems'),
(27, NULL, 'Longitud(cm)', 1, 1, NULL, 'cm', NULL, NULL),
(36, NULL, 'Proveedor', 1, 0, NULL, NULL, NULL, NULL),
(36, 'producer', 'Proveedor', 1, 0, NULL, NULL, NULL, 'producer'),
(56, NULL, 'Genero', 1, 0, NULL, NULL, NULL, NULL),
(58, NULL, 'Variedad', 1, 0, NULL, NULL, NULL, NULL),
(67, 'category', 'Categoria', 1, 0, NULL, NULL, NULL, NULL),
@ -2625,7 +2625,7 @@ INSERT INTO `vn`.`machineWorker` (`workerFk`, `machineFk`, `inTimed`, `outTimed`
INSERT INTO `vn`.`zoneExclusion` (`id`, `zoneFk`, `dated`, `created`, `userFk`)
VALUES
(1, 1, DATE_ADD(CURDATE(), INTERVAL (IF(DAYOFWEEK(CURDATE())<=7, 7, 14) - DAYOFWEEK(util.VN_CURDATE())) DAY), util.VN_CURDATE(), 100),
(1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL (IF(DAYOFWEEK(util.VN_CURDATE())<=7, 7, 14) - DAYOFWEEK(util.VN_CURDATE())) DAY), util.VN_CURDATE(), 100),
(2, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL (IF(DAYOFWEEK(util.VN_CURDATE())<=8, 8, 15) - DAYOFWEEK(util.VN_CURDATE())) DAY), util.VN_CURDATE(), 100);
INSERT INTO `vn`.`zoneExclusionGeo` (`zoneExclusionFk`, `geoFk`)
@ -2663,9 +2663,9 @@ INSERT INTO `vn`.`sectorCollectionSaleGroup` (`sectorCollectionFk`, `saleGroupFk
VALUES
(1, 1);
INSERT INTO `vn`.`workerTimeControlConfig` (`id`, `dayBreak`, `dayBreakDriver`, `shortWeekBreak`, `longWeekBreak`, `weekScope`, `mailPass`, `mailHost`, `mailSuccessFolder`, `mailErrorFolder`, `mailUser`, `minHoursToBreak`, `breakHours`, `hoursCompleteWeek`, `startNightlyHours`, `endNightlyHours`, `maxTimePerDay`, `breakTime`, `timeToBreakTime`, `dayMaxTime`, `shortWeekDays`, `longWeekDays`)
INSERT INTO `vn`.`workerTimeControlConfig` (`id`, `dayBreak`, `dayBreakDriver`, `shortWeekBreak`, `longWeekBreak`, `weekScope`, `mailPass`, `mailHost`, `mailSuccessFolder`, `mailErrorFolder`, `mailUser`, `minHoursToBreak`, `breakHours`, `hoursCompleteWeek`, `startNightlyHours`, `endNightlyHours`, `maxTimePerDay`, `breakTime`, `timeToBreakTime`, `dayMaxTime`, `shortWeekDays`, `longWeekDays`, `teleworkingStart`, `teleworkingStartBreakTime`)
VALUES
(1, 43200, 32400, 129600, 259200, 604800, '', '', 'Leidos.exito', 'Leidos.error', 'timeControl', 5.33, 0.33, 40, '22:00:00', '06:00:00', 57600, 1200, 18000, 57600, 6, 13);
(1, 43200, 32400, 129600, 259200, 604800, '', '', 'Leidos.exito', 'Leidos.error', 'timeControl', 5.33, 0.33, 40, '22:00:00', '06:00:00', 57600, 1200, 18000, 57600, 6, 13, 28800, 32400);
INSERT INTO `vn`.`host` (`id`, `code`, `description`, `warehouseFk`, `bankFk`)
VALUES
@ -2715,6 +2715,10 @@ INSERT INTO `vn`.`collection` (`id`, `created`, `workerFk`, `stateFk`, `itemPack
VALUES
(3, util.VN_NOW(), 1107, 5, NULL, 0, 0, 1, NULL, NULL);
INSERT INTO `vn`.`itemConfig` (`id`, `isItemTagTriggerDisabled`, `monthToDeactivate`, `wasteRecipients`, `validPriorities`, `defaultPriority`, `defaultTag`)
VALUES
(0, 0, 24, '', '[1,2,3]', 2, 56);
INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `level`, `wagon`, `smartTagFk`, `usedShelves`, `itemCount`, `liters`)
VALUES
(9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL);
@ -2737,13 +2741,21 @@ INSERT INTO `vn`.`ticketLog` (`originFk`, userFk, `action`, changedModel, oldIns
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', '1,6', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
INSERT INTO `vn`.`mdbApp` (`app`, `baselineBranchFk`, `userFk`, `locked`)
VALUES
('foo', 'master', NULL, NULL),
('bar', 'test', 9, util.VN_NOW());
INSERT INTO `vn`.`profileType` (`id`, `name`)
VALUES
(1, 'working');
INSERT INTO `vn`.`newWorkerConfig` (`id`, `street`, `provinceFk`, `companyFk`, `profileTypeFk`, `roleFk`)
VALUES
(1, 'S/ ', 1, 442, 1, 1);
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('lilium', 'dev', 'http://localhost:8080/#/'),
@ -2753,3 +2765,7 @@ INSERT INTO `vn`.`payDemDetail` (`id`, `detail`)
VALUES
(1, 1);
INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`)
VALUES
(1, NULL, 1);

View File

@ -58,6 +58,7 @@ export default {
deleteAccount: '.vn-menu [name="deleteUser"]',
setPassword: '.vn-menu [name="setPassword"]',
activateAccount: '.vn-menu [name="enableAccount"]',
disableAccount: '.vn-menu [name="disableAccount"]',
activateUser: '.vn-menu [name="activateUser"]',
deactivateUser: '.vn-menu [name="deactivateUser"]',
newPassword: 'vn-textfield[ng-model="$ctrl.newPassword"]',
@ -428,6 +429,7 @@ export default {
},
itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
priority: 'vn-autocomplete[ng-model="$ctrl.item.priority"]',
type: 'vn-autocomplete[ng-model="$ctrl.item.typeFk"]',
intrastat: 'vn-autocomplete[ng-model="$ctrl.item.intrastatFk"]',
origin: 'vn-autocomplete[ng-model="$ctrl.item.originFk"]',
@ -521,7 +523,7 @@ export default {
},
itemLog: {
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
fifthLineCreatedProperty: 'vn-item-log > vn-log vn-tbody > vn-tr:nth-child(5) table tr:nth-child(3) td.after',
fifthLineCreatedProperty: 'vn-item-log > vn-log vn-tbody > vn-tr:nth-child(5) table tr:nth-child(2) td.after',
},
ticketSummary: {
header: 'vn-ticket-summary > vn-card > h5',
@ -968,6 +970,7 @@ export default {
confirmButton: '.vn-confirm.shown button[response="accept"]'
},
workerSummary: {
summaryIcon: 'vn-worker-descriptor a[title="Go to module summary"]',
header: 'vn-worker-summary h5',
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',
@ -1020,6 +1023,25 @@ export default {
furlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(4)',
halfFurlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(5)',
},
workerCreate: {
newWorkerButton: 'vn-worker-index a[ui-sref="worker.create"]',
firstname: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.firstName"]',
lastname: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.lastNames"]',
birth: 'vn-worker-create vn-date-picker[ng-model="$ctrl.worker.birth"]',
fi: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.fi"]',
code: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.code"]',
phone: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.phone"]',
city: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.city"]',
postcode: 'vn-worker-create vn-datalist[ng-model="$ctrl.worker.postcode"]',
street: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.street"]',
user: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.name"]',
email: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.email"]',
boss: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bossFk"]',
role: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.roleFk"]',
iban: 'vn-worker-create vn-textfield[ng-model="$ctrl.worker.iban"]',
switft: 'vn-worker-create vn-autocomplete[ng-model="$ctrl.worker.bankEntityFk"]',
createButton: 'vn-worker-create vn-submit[label="Create"]',
},
invoiceOutIndex: {
topbarSearch: 'vn-searchbar',
searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr',

View File

@ -0,0 +1,74 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Worker create path', () => {
let browser;
let page;
let newWorker;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('hr', 'worker');
await page.waitToClick(selectors.workerCreate.newWorkerButton);
await page.waitForState('worker.create');
});
afterAll(async() => {
await browser.close();
});
it('should insert default data', async() => {
await page.write(selectors.workerCreate.firstname, 'Victor');
await page.write(selectors.workerCreate.lastname, 'Von Doom');
await page.write(selectors.workerCreate.fi, '78457139E');
await page.write(selectors.workerCreate.phone, '12356789');
await page.write(selectors.workerCreate.postcode, '46680');
await page.write(selectors.workerCreate.street, 'S/ Doomstadt');
await page.write(selectors.workerCreate.email, 'doctorDoom@marvel.com');
await page.write(selectors.workerCreate.iban, 'ES9121000418450200051332');
await page.autocompleteSearch(selectors.workerCreate.switft, 'BBKKESMMMMM');
// should check for autocompleted worker code and worker user name
const workerCode = await page
.waitToGetProperty(selectors.workerCreate.code, 'value');
newWorker = await page
.waitToGetProperty(selectors.workerCreate.user, 'value');
expect(workerCode).toEqual('VVD');
expect(newWorker).toContain('victorvd');
// should fail if necessary data is void
await page.waitToClick(selectors.workerCreate.createButton);
let message = await page.waitForSnackbar();
expect(message.text).toContain('is a required argument');
// should create a new worker and go to worker basic data'
await page.pickDate(selectors.workerCreate.birth, new Date(1962, 8, 5));
await page.autocompleteSearch(selectors.workerCreate.boss, 'deliveryBoss');
await page.waitToClick(selectors.workerCreate.createButton);
message = await page.waitForSnackbar();
await page.waitForState('worker.card.basicData');
expect(message.text).toContain('Data saved!');
// 'rollback'
await page.loginAndModule('sysadmin', 'account');
await page.accessToSearchResult(newWorker);
await page.waitToClick(selectors.accountDescriptor.menuButton);
await page.waitToClick(selectors.accountDescriptor.deactivateUser);
await page.waitToClick(selectors.accountDescriptor.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('User deactivated!');
await page.waitToClick(selectors.accountDescriptor.menuButton);
await page.waitToClick(selectors.accountDescriptor.disableAccount);
await page.waitToClick(selectors.accountDescriptor.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('Account disabled!');
});
});

View File

@ -36,11 +36,20 @@ describe('Item Create', () => {
await page.waitForState('item.create');
});
it('should create the Infinity Gauntlet item', async() => {
it('should throw an error when insert an invalid priority', async() => {
await page.write(selectors.itemCreateView.temporalName, 'Infinity Gauntlet');
await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo');
await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares');
await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand');
await page.clearInput(selectors.itemCreateView.priority);
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Valid priorities');
});
it('should create the Infinity Gauntlet item', async() => {
await page.autocompleteSearch(selectors.itemCreateView.priority, '2');
await page.waitToClick(selectors.itemCreateView.createButton);
const message = await page.waitForSnackbar();

View File

@ -1,6 +1,6 @@
Reset password: Restrablecer contraseña
New password: Nueva contraseña
Repeat password: Repetir contraseñaç
Repeat password: Repetir contraseña
Password changed!: ¡Contraseña cambiada!
Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

@ -252,6 +252,9 @@
"Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente",
"This worker code already exists": "Este codigo de trabajador ya existe",
"This personal mail already exists": "Este correo personal ya existe",
"This worker already exists": "Este trabajador ya existe",
"App name does not exist": "El nombre de aplicación no es válido",
"Try again": "Vuelve a intentarlo",
"Aplicación bloqueada por el usuario 9": "Aplicación bloqueada por el usuario 9",

View File

@ -36,13 +36,13 @@
ng-if="$ctrl.isAgricultural()"
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
Show agricultural receipt as PDF
</vn-item>
<vn-item
ng-if="$ctrl.isAgricultural()"
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF
Send agricultural receipt as PDF
</vn-item>
</slot-menu>
<slot-body>

View File

@ -19,6 +19,6 @@ To book: Contabilizar
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF
Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
New InvoiceIn: Nueva Factura

View File

@ -37,7 +37,8 @@ module.exports = Self => {
'typeFk',
'intrastatFk',
'originFk',
'relevancy'
'priority',
'tag'
];
for (const key in params) {
@ -46,10 +47,14 @@ module.exports = Self => {
}
try {
const itemConfig = await models.ItemConfig.findOne({fields: ['validPriorities']}, myOptions);
if (!itemConfig.validPriorities.includes(params.priority))
throw new UserError(`Valid priorities: ${[...itemConfig.validPriorities]}`);
const provisionalName = params.provisionalName;
delete params.provisionalName;
const itemType = await models.ItemType.findById(params.typeFk, myOptions);
const itemType = await models.ItemType.findById(params.typeFk, {fields: ['isLaid']}, myOptions);
params.isLaid = itemType.isLaid;
@ -63,12 +68,13 @@ module.exports = Self => {
await Self.rawSql(query, null, myOptions);
let nameTag = await models.Tag.findOne({where: {name: 'Nombre temporal'}});
const nameTag = await models.Tag.findById(params.tag, {fields: ['id']}, myOptions);
let newTags = [];
newTags.push({itemFk: item.id, tagFk: nameTag.id, value: provisionalName, priority: '2'});
newTags.push({itemFk: item.id, tagFk: nameTag.id, value: provisionalName, priority: item.priority});
typeTags.forEach(typeTag => {
if (nameTag.id != typeTag.tagFk)
newTags.push({itemFk: item.id, tagFk: typeTag.tagFk, value: '', priority: typeTag.priority});
});

View File

@ -11,7 +11,8 @@ describe('item new()', () => {
originFk: 1,
provisionalName: 'planta',
typeFk: 2,
relevancy: 0
priority: 2,
tag: 1
};
let item = await models.Item.new(itemParams, options);
@ -20,7 +21,7 @@ describe('item new()', () => {
item.isLaid = itemType.isLaid;
const temporalNameTag = await models.Tag.findOne({where: {name: 'Nombre temporal'}}, options);
const temporalNameTag = await models.Tag.findById(itemParams.tag, {fields: ['id']}, options);
const temporalName = await models.ItemTag.findOne({
where: {
@ -31,7 +32,7 @@ describe('item new()', () => {
item = await models.Item.findById(item.id, null, options);
itemType = await models.ItemType.findById(item.typeFk, options);
itemType = await models.ItemType.findById(item.typeFk, {fields: ['isLaid']}, options);
item.isLaid = itemType.isLaid;

View File

@ -16,6 +16,22 @@
"wasteRecipients": {
"type": "string",
"description": "Buyers waste report recipients"
},
"validPriorities": {
"type": "array"
},
"defaultPriority": {
"type": "int"
},
"defaultTag": {
"type": "int"
}
},
"relations": {
"tag": {
"type": "belongsTo",
"model": "Tag",
"foreignKey": "defaultTag"
}
}
}

View File

@ -13,6 +13,9 @@
},
"description": {
"type": "string"
},
"isActive":{
"type": "boolean"
}
},
"acls": [

View File

@ -16,10 +16,24 @@
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Temporal name"
label="Name"
ng-model="$ctrl.item.provisionalName"
vn-focus>
</vn-textfield>
<vn-autocomplete
ng-model="$ctrl.item.tag"
url="Tags"
show-field="name"
value-field="id"
label="Tag">
</vn-autocomplete>
<vn-autocomplete
data="$ctrl.validPriorities"
label="Priority"
ng-model="$ctrl.item.priority"
show-field="priority"
value-field="priority">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete

View File

@ -4,10 +4,26 @@ import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.fetchDefaultPriorityTag();
}
fetchDefaultPriorityTag() {
this.validPriorities = [];
const filter = {fields: ['defaultPriority', 'defaultTag', 'validPriorities'], limit: 1};
this.$http.get(`ItemConfigs`, {filter})
.then(res => {
if (res.data) {
const dataRow = res.data[0];
dataRow.validPriorities.forEach(priority => {
this.validPriorities.push({priority});
});
this.item = {
relevancy: 0
priority: dataRow.defaultPriority,
tag: dataRow.defaultTag
};
}
});
}
onSubmit() {
this.$.watcher.submit().then(

View File

@ -25,7 +25,10 @@ class Controller extends SearchPanel {
getItemPackingTypes() {
let itemPackingTypes = [];
this.$http.get('ItemPackingTypes').then(res => {
const filter = {
where: {isActive: true}
};
this.$http.get('ItemPackingTypes', {filter}).then(res => {
for (let ipt of res.data) {
itemPackingTypes.push({
code: ipt.code,

View File

@ -39,6 +39,7 @@ export default class Controller extends Section {
field: 'ipt',
autocomplete: {
url: 'ItemPackingTypes',
where: `{isActive: true}`,
showField: 'description',
valueField: 'code'
}
@ -47,6 +48,7 @@ export default class Controller extends Section {
field: 'futureIpt',
autocomplete: {
url: 'ItemPackingTypes',
where: `{isActive: true}`,
showField: 'description',
valueField: 'code'
}

View File

@ -25,7 +25,10 @@ class Controller extends SearchPanel {
getItemPackingTypes() {
let itemPackingTypes = [];
this.$http.get('ItemPackingTypes').then(res => {
const filter = {
where: {isActive: true}
};
this.$http.get('ItemPackingTypes', {filter}).then(res => {
for (let ipt of res.data) {
itemPackingTypes.push({
description: this.$t(ipt.description),

View File

@ -134,7 +134,7 @@
{{::ticket.shipped | date: 'dd/MM/yyyy HH:mm'}}
</span>
</td>
<td>{{::ticket.ipt}}</td>
<td>{{::ticket.ipt | dashIfEmpty}}</td>
<td>
<span
class="chip {{$ctrl.stateColor(ticket.state)}}">
@ -155,7 +155,7 @@
{{::ticket.futureShipped | date: 'dd/MM/yyyy HH:mm'}}
</span>
</td>
<td>{{::ticket.futureIpt}}</td>
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
class="chip {{$ctrl.stateColor(ticket.futureState)}}">

View File

@ -34,6 +34,7 @@ export default class Controller extends Section {
field: 'ipt',
autocomplete: {
url: 'ItemPackingTypes',
where: `{isActive: true}`,
showField: 'description',
valueField: 'code'
}
@ -42,6 +43,7 @@ export default class Controller extends Section {
field: 'futureIpt',
autocomplete: {
url: 'ItemPackingTypes',
where: `{isActive: true}`,
showField: 'description',
valueField: 'code'
}

View File

@ -32,22 +32,15 @@ module.exports = Self => {
const models = Self.app.models;
const conn = Self.dataSource.connector;
const args = ctx.args;
const $t = ctx.req.__; // $translate
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
const stmts = [];
let stmt;
try {
if (!args.week || !args.year) {
const from = new Date();
const to = new Date();
@ -156,6 +149,9 @@ module.exports = Self => {
`, [args.workerId]);
const index = stmts.push(stmt) - 1;
stmts.push('DROP TEMPORARY TABLE tmp.timeControlCalculate');
stmts.push('DROP TEMPORARY TABLE tmp.timeBusinessCalculate');
const sql = ParameterizedSQL.join(stmts, ';');
const days = await conn.executeStmt(sql, myOptions);
@ -165,14 +161,19 @@ module.exports = Self => {
const workerTimeControlConfig = await models.WorkerTimeControlConfig.findOne(null, myOptions);
for (let day of days[index]) {
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
workerFk = day.workerFk;
if (day.timeWorkDecimal > 0 && day.timeWorkedDecimal == null
&& (day.permissionRate ? day.permissionRate : true)) {
&& (day.permissionRate == null ? true : day.permissionRate)) {
if (day.timeTable == null) {
const timed = new Date(day.dated);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(8),
timed: timed.setHours(workerTimeControlConfig.teleworkingStart / 3600),
manual: true,
direction: 'in',
isSendMail: true
@ -181,7 +182,7 @@ module.exports = Self => {
if (day.timeWorkDecimal >= workerTimeControlConfig.timeToBreakTime / 3600) {
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(9),
timed: timed.setHours(workerTimeControlConfig.teleworkingStartBreakTime / 3600),
manual: true,
direction: 'middle',
isSendMail: true
@ -189,7 +190,10 @@ module.exports = Self => {
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(9, 20),
timed: timed.setHours(
workerTimeControlConfig.teleworkingStartBreakTime / 3600,
workerTimeControlConfig.breakTime / 60
),
manual: true,
direction: 'middle',
isSendMail: true
@ -199,7 +203,11 @@ module.exports = Self => {
const [hoursWork, minutesWork, secondsWork] = getTime(day.timeWorkSexagesimal);
await models.WorkerTimeControl.create({
userFk: day.workerFk,
timed: timed.setHours(8 + hoursWork, minutesWork, secondsWork),
timed: timed.setHours(
workerTimeControlConfig.teleworkingStart / 3600 + hoursWork,
minutesWork,
secondsWork
),
manual: true,
direction: 'out',
isSendMail: true
@ -307,7 +315,7 @@ module.exports = Self => {
}, myOptions);
if (firstWorkerTimeControl)
firstWorkerTimeControl.updateAttribute('direction', 'in', myOptions);
await firstWorkerTimeControl.updateAttribute('direction', 'in', myOptions);
const lastWorkerTimeControl = await models.WorkerTimeControl.findOne({
where: {
@ -318,7 +326,7 @@ module.exports = Self => {
}, myOptions);
if (lastWorkerTimeControl)
lastWorkerTimeControl.updateAttribute('direction', 'out', myOptions);
await lastWorkerTimeControl.updateAttribute('direction', 'out', myOptions);
}
}
@ -339,15 +347,18 @@ module.exports = Self => {
previousWorkerFk = day.workerFk;
previousReceiver = day.receiver;
}
if (tx) {
await tx.commit();
delete myOptions.transaction;
}
if (tx) await tx.commit();
return true;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}
return true;
};
function getStartDateOfWeekNumber(week, year) {

View File

@ -10,7 +10,6 @@ describe('workerTimeControl sendMail()', () => {
const ctx = {req: activeCtx, args: {}};
it('should fill time control of a worker without records in Journey and with rest', async() => {
pending('https://redmine.verdnatura.es/issues/4903');
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
@ -35,7 +34,6 @@ describe('workerTimeControl sendMail()', () => {
});
it('should fill time control of a worker without records in Journey and without rest', async() => {
pending('https://redmine.verdnatura.es/issues/4903');
const workdayOf20Hours = 3;
const tx = await models.WorkerTimeControl.beginTransaction({});
@ -63,7 +61,6 @@ describe('workerTimeControl sendMail()', () => {
});
it('should fill time control of a worker with records in Journey and with rest', async() => {
pending('https://redmine.verdnatura.es/issues/4903');
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
@ -95,7 +92,6 @@ describe('workerTimeControl sendMail()', () => {
});
it('should fill time control of a worker with records in Journey and without rest', async() => {
pending('https://redmine.verdnatura.es/issues/4903');
const tx = await models.WorkerTimeControl.beginTransaction({});
try {

View File

@ -2,7 +2,7 @@ const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('weeklyHourRecordEmail', {
description: 'Sends the buyer waste email',
description: 'Sends the weekly hour record',
accessType: 'WRITE',
accepts: [
{

View File

@ -0,0 +1,256 @@
/* eslint max-len: ["error", { "code": 130 }]*/
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('new', {
description: 'Creates a new worker and returns the id',
accessType: 'WRITE',
accepts: [
{
arg: 'fi',
type: 'string',
description: `The worker fi`,
required: true,
},
{
arg: 'name',
type: 'string',
description: `The user name`,
required: true,
},
{
arg: 'firstName',
type: 'string',
description: `The worker firstname`,
required: true,
},
{
arg: 'lastNames',
type: 'string',
description: `The worker lastnames`,
required: true,
},
{
arg: 'email',
type: 'string',
description: `The worker email`,
required: true,
},
{
arg: 'street',
type: 'string',
description: `The worker address`,
required: true,
},
{
arg: 'city',
type: 'string',
description: `The worker city`,
required: true,
},
{
arg: 'provinceFk',
type: 'number',
description: `The worker province`,
required: true,
},
{
arg: 'iban',
type: 'string',
description: `The worker iban`,
required: true,
},
{
arg: 'bankEntityFk',
type: 'number',
description: `The worker bank entity`,
required: true,
},
{
arg: 'companyFk',
type: 'number',
description: `The worker company`,
required: true,
},
{
arg: 'postcode',
type: 'string',
description: `The worker postcode`,
required: true,
},
{
arg: 'phone',
type: 'string',
description: `The worker phone`,
required: true,
},
{
arg: 'code',
type: 'string',
description: `The worker code`,
required: true,
},
{
arg: 'bossFk',
type: 'number',
description: `The worker boss`,
required: true,
},
{
arg: 'birth',
type: 'date',
description: `The worker birth`,
required: true,
}
],
returns: {
type: 'number',
root: true,
},
http: {
path: `/new`,
verb: 'POST',
},
});
Self.new = async(ctx, options) => {
const models = Self.app.models;
const myOptions = {};
const args = ctx.args;
let tx;
if (typeof options == 'object') Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
let client;
try {
client = await models.Client.findOne(
{
where: {fi: args.fi},
},
myOptions
);
if (!client) {
const nickname = args.firstName.concat(' ', args.lastNames);
const workerConfig = await models.WorkerConfig.findOne({fields: ['roleFk']});
const [randomPassword] = await models.Worker.rawSql(
'SELECT account.passwordGenerate() as password;'
);
const user = await models.Account.create(
{
name: args.name,
nickname,
password: randomPassword.password,
email: args.email,
roleFk: workerConfig.roleFk,
},
myOptions
);
await models.UserAccount.create(
{
id: user.id,
},
myOptions
);
await models.Worker.rawSql(
'CALL vn.clientCreate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
args.firstName,
args.lastNames,
args.fi,
args.street,
args.postalCode,
args.city,
args.provinceFk,
args.companyFk,
args.phone,
args.email,
user.id,
],
myOptions
);
const address = await models.Address.create(
{
clientFk: user.id,
street: args.street,
city: args.city,
provinceFk: args.provinceFk,
postalCode: args.postalCode,
mobile: args.phone,
nickname: nickname,
isDefaultAddress: true,
},
myOptions
);
client = await models.Client.findById(
user.id,
{fields: ['id', 'name', 'socialName', 'street', 'city', 'iban', 'bankEntityFk', 'defaultAddressFk']},
myOptions
);
await client.updateAttributes(
{
iban: args.iban,
bankEntityFk: args.bankEntityFk,
defaultAddressFk: address.id,
},
myOptions
);
}
const user = await models.Account.findById(client.id, null, myOptions);
await user.updateAttribute('email', args.email, myOptions);
await models.Worker.rawSql(
'CALL vn.workerCreate(?, ?, ?, ?, ?, ?, ?)',
[
args.firstName,
args.lastNames,
args.code,
args.bossFk,
client.id,
args.fi,
args.birth,
],
myOptions
);
if (tx) await tx.commit();
} catch (error) {
if (tx) await tx.rollback();
const code = error.code;
const message = error.sqlMessage;
if (code === 'ER_DUP_ENTRY' && message.includes(`for key 'mail'`))
throw new UserError(`This personal mail already exists`);
if (code === 'ER_DUP_ENTRY' && message.includes(`CodigoTrabajador_UNIQUE`))
throw new UserError(`This worker code already exists`);
if (code === 'ER_DUP_ENTRY' && message.includes(`PRIMARY`))
throw new UserError(`This worker already exists`);
throw error;
}
await models.user.resetPassword({
email: args.email,
emailTemplate: 'worker-welcome',
id: client.id
});
return {id: client.id};
};
};

View File

@ -0,0 +1,139 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Worker new', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const employeeId = 1;
const defaultWorker = {
fi: '78457139E',
name: 'defaultWorker',
firstName: 'default',
lastNames: 'worker',
email: 'defaultWorker@mydomain.com',
street: 'S/ defaultWorkerStreet',
city: 'defaultWorkerCity',
provinceFk: 1,
iban: 'ES8304879798578129532677',
bankEntityFk: 128,
companyFk: 442,
postcode: '46680',
phone: '123456789',
code: 'DWW',
bossFk: 9,
birth: '2022-12-11T23:00:00.000Z'
};
it('should return error if personal mail already exists', async() => {
const user = await models.Account.findById(employeeId, {fields: ['email']});
const tx = await models.Worker.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
args: Object.assign({}, defaultWorker, {email: user.email})
};
await models.Worker.new(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual('This personal mail already exists');
});
it('should return error if worker code already exists', async() => {
const worker = await models.Worker.findById(employeeId, {fields: ['code']});
const tx = await models.Worker.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
args: Object.assign({}, defaultWorker, {code: worker.code})
};
await models.Worker.new(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual('This worker code already exists');
});
it('should return error if worker already exists', async() => {
const worker = await models.Client.findById(employeeId, {fields: ['fi']});
const tx = await models.Worker.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
args: Object.assign({}, defaultWorker, {fi: worker.fi})
};
await models.Worker.new(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual('This worker already exists');
});
it('should create a new worker', async() => {
const newWorker = await models.Worker.new({args: defaultWorker});
await models.Worker.destroyById(newWorker.id);
await models.Address.destroyAll({clientFk: newWorker.id});
await models.Mandate.destroyAll({clientFk: newWorker.id});
await models.Client.destroyById(newWorker.id);
await models.Account.destroyById(newWorker.id);
expect(newWorker.id).toBeDefined();
});
it('should create a new worker in client', async() => {
const bruceWayneId = 1101;
const client = await models.Client.findById(bruceWayneId, {fields: ['fi', 'email']});
const newWorkerData = {
args: Object.assign(
{},
defaultWorker,
{
fi: client.fi,
email: client.email
})
};
const newWorker = await models.Worker.new(newWorkerData);
await models.Worker.destroyById(newWorker.id);
expect(newWorker.id).toEqual(bruceWayneId);
});
});

View File

@ -17,12 +17,18 @@
"Department": {
"dataSource": "vn"
},
"Device": {
"dataSource": "vn"
},
"EducationLevel": {
"dataSource": "vn"
},
"Journey": {
"dataSource": "vn"
},
"ProfileType":{
"dataSource": "vn"
},
"Time": {
"dataSource": "vn"
},
@ -32,39 +38,42 @@
"WorkCenterHoliday": {
"dataSource": "vn"
},
"WorkerDms": {
"Worker": {
"dataSource": "vn"
},
"Worker": {
"WorkerConfig": {
"dataSource": "vn"
},
"WorkerDepartment": {
"dataSource": "vn"
},
"WorkerDisableExcluded": {
"dataSource": "vn"
},
"WorkerDms": {
"dataSource": "vn"
},
"WorkerLabour": {
"dataSource": "vn"
},
"WorkerLog": {
"dataSource": "vn"
},
"WorkerMana": {
"dataSource": "vn"
},
"WorkerMedia": {
"dataSource": "vn"
},
"WorkerTeam": {
"dataSource": "vn"
},
"WorkerTeamCollegues": {
"dataSource": "vn"
},
"WorkerMedia": {
"dataSource": "vn"
},
"WorkerDepartment": {
"dataSource": "vn"
},
"WorkerTimeControl": {
"dataSource": "vn"
},
"Device": {
"dataSource": "vn"
},
"WorkerLog": {
"dataSource": "vn"
},
"WorkerTimeControlConfig": {
"dataSource": "vn"
},
@ -73,9 +82,6 @@
},
"WorkerTimeControlMail": {
"dataSource": "vn"
},
"WorkerDisableExcluded": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "ProfileType",
"base": "VnModel",
"options": {
"mysql": {
"table": "profileType"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"name": {
"type": "string"
}
}
}

View File

@ -0,0 +1,27 @@
{
"name": "WorkerConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "workerConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"roleFk": {
"type": "number"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -11,8 +11,17 @@
"id": true,
"type": "number"
},
"breakTime": {
"type": "number"
},
"timeToBreakTime": {
"type": "number"
},
"teleworkingStart": {
"type": "number"
},
"teleworkingStartBreakTime": {
"type": "number"
}
}
}

View File

@ -13,4 +13,5 @@ module.exports = Self => {
require('../methods/worker/contracts')(Self);
require('../methods/worker/holidays')(Self);
require('../methods/worker/activeContract')(Self);
require('../methods/worker/new')(Self);
};

View File

@ -52,6 +52,9 @@
},
"mobileExtension": {
"type" : "number"
},
"code": {
"type" : "string"
}
},
"relations": {

View File

@ -0,0 +1,194 @@
<vn-watcher
vn-id="watcher"
url="Workers/new"
data="$ctrl.worker"
insert-mode="true"
form="form">
</vn-watcher>
<form name="form" vn-http-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
vn-one
label="Firstname"
ng-model="$ctrl.worker.firstName"
rule
on-change="$ctrl.generateCodeUser()"
vn-focus>
</vn-textfield>
<vn-textfield
vn-one
label="Lastname"
on-change="$ctrl.generateCodeUser()"
ng-model="$ctrl.worker.lastNames"
rule>
</vn-textfield>
<vn-date-picker
vn-one
label="Birth"
ng-model="$ctrl.worker.birth">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Fi"
ng-model="$ctrl.worker.fi"
rule>
</vn-textfield>
<vn-textfield
vn-one
label="Code"
ng-model="$ctrl.worker.code"
maxLength="3"
on-change="$ctrl.worker.code = $ctrl.worker.code.toUpperCase()"
rule>
</vn-textfield>
<vn-textfield
vn-one
label="Phone"
ng-model="$ctrl.worker.phone"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-datalist
label="Postcode"
vn-one
ng-model="$ctrl.worker.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-autocomplete
vn-id="province"
label="Province"
ng-model="$ctrl.worker.provinceFk"
selection="$ctrl.province"
url="Provinces/location"
fields="['id', 'name', 'countryFk']"
rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-datalist
vn-id="town"
label="City"
ng-model="$ctrl.worker.city"
selection="$ctrl.town"
url="Towns/location"
fields="['id', 'name', 'provinceFk']"
value-field="name">
<tpl-item>
{{name}}, {{province.name}}
({{province.country.country}})
</tpl-item>
</vn-datalist>
<vn-textfield
vn-two
label="Street"
ng-model="$ctrl.worker.street"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Web user"
ng-model="$ctrl.worker.name"
rule>
</vn-textfield>
<vn-textfield
label="Personal email"
ng-model="$ctrl.worker.email"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="company"
ng-model="$ctrl.worker.companyFk"
url="Companies"
show-field="code"
value-field="id"
label="Company">
</vn-autocomplete>
<vn-autocomplete
vn-one
ng-model="$ctrl.worker.bossFk"
url="Workers/activeWithInheritedRole"
show-field="nickname"
search-function="{firstName: $search}"
where="{role: 'employee'}"
label="Boss">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="IBAN"
ng-model="$ctrl.worker.iban"
rule
on-change="$ctrl.autofillBic()">
</vn-textfield>
<vn-autocomplete
vn-one
label="Swift / BIC"
url="BankEntities"
ng-model="$ctrl.worker.bankEntityFk"
fields="['name']"
initial-data="$ctrl.worker.bankEntityFk"
on-change="$ctrl.autofillBic()"
search-function="{or: [{bic: {like: $search +'%'}}, {name: {like: '%'+ $search +'%'}}]}"
value-field="id"
show-field="bic"
vn-acl="salesAssistant, hr"
disabled="$ctrl.ibanCountry == 'ES'">
<tpl-item>{{bic}} {{name}}</tpl-item>
<append>
<vn-icon-button
vn-auto
icon="add_circle"
vn-click-stop="bankEntity.show({countryFk: $ctrl.worker.countryFk})"
vn-tooltip="New bank entity"
vn-acl="salesAssistant, hr">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Create">
</vn-submit>
<vn-button
class="cancel"
label="Cancel"
ui-sref="worker.index">
</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,127 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.worker = {companyFk: this.vnConfig.user.companyFk};
}
onSubmit() {
return this.$.watcher.submit().then(json => {
this.$state.go('worker.card.basicData', {id: json.data.id});
});
}
autofillBic() {
if (!this.worker || !this.worker.iban) return;
let bankEntityId = parseInt(this.worker.iban.substr(4, 4));
let filter = {where: {id: bankEntityId}};
if (this.ibanCountry != 'ES') return;
this.$http.get(`BankEntities`, {filter}).then(response => {
const hasData = response.data && response.data[0];
if (hasData)
this.worker.bankEntityFk = response.data[0].id;
else if (!hasData)
this.worker.bankEntityFk = null;
});
}
generateCodeUser() {
if (!this.worker.firstName || !this.worker.lastNames) return;
const totalName = this.worker.firstName.concat(' ' + this.worker.lastNames).toLowerCase();
const totalNameArray = totalName.split(' ');
let newCode = '';
for (let part of totalNameArray)
newCode += part.charAt(0);
this.worker.code = newCode.toUpperCase().slice(0, 3);
this.worker.name = totalNameArray[0] + newCode.slice(1);
if (!this.worker.companyFk)
this.worker.companyFk = this.vnConfig.user.companyFk;
}
get province() {
return this._province;
}
// Province auto complete
set province(selection) {
this._province = selection;
if (!selection) return;
const country = selection.country;
if (!this.worker.countryFk)
this.worker.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.worker.provinceFk)
this.worker.provinceFk = province.id;
if (!this.worker.countryFk)
this.worker.countryFk = country.id;
if (postcodes.length === 1)
this.worker.postcode = postcodes[0].code;
}
get postcode() {
return this._postcode;
}
// Postcode auto complete
set postcode(selection) {
this._postcode = selection;
if (!selection) return;
const town = selection.town;
const province = town.province;
const country = province.country;
if (!this.worker.city)
this.worker.city = town.name;
if (!this.worker.provinceFk)
this.worker.provinceFk = province.id;
if (!this.worker.countryFk)
this.worker.countryFk = country.id;
}
onResponse(response) {
this.worker.postcode = response.code;
this.worker.city = response.city;
this.worker.provinceFk = response.provinceFk;
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnWorkerCreate', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,133 @@
import './index';
describe('Worker', () => {
describe('Component vnWorkerCreate', () => {
let $scope;
let $state;
let controller;
beforeEach(ngModule('worker'));
beforeEach(inject(($componentController, $rootScope, _$state_) => {
$scope = $rootScope.$new();
$state = _$state_;
$scope.watcher = {
submit: () => {
return {
then: callback => {
callback({data: {id: '1234'}});
}
};
}
};
const $element = angular.element('<vn-worker-create></vn-worker-create>');
controller = $componentController('vnWorkerCreate', {$element, $scope});
controller.worker = {};
controller.vnConfig = {user: {companyFk: 1}};
}));
describe('onSubmit()', () => {
it(`should call submit() on the watcher then expect a callback`, () => {
jest.spyOn($state, 'go');
controller.onSubmit();
expect(controller.$state.go).toHaveBeenCalledWith('worker.card.basicData', {id: '1234'});
});
});
describe('province() setter', () => {
it(`should set countryFk property`, () => {
controller.worker.countryFk = null;
controller.province = {
id: 1,
name: 'New york',
country: {
id: 2,
name: 'USA'
}
};
expect(controller.worker.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.worker.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.worker.provinceFk).toEqual(1);
expect(controller.worker.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.worker.city).toEqual('New York');
expect(controller.worker.provinceFk).toEqual(1);
expect(controller.worker.countryFk).toEqual(2);
});
});
describe('generateCodeUser()', () => {
it(`should generate worker code, name and company `, () => {
controller.worker = {
firstName: 'default',
lastNames: 'generate worker'
};
controller.generateCodeUser();
expect(controller.worker.code).toEqual('DGW');
expect(controller.worker.name).toEqual('defaultgw');
expect(controller.worker.companyFk).toEqual(controller.vnConfig.user.companyFk);
});
});
});
});

View File

@ -0,0 +1,12 @@
Firstname: Nombre
Lastname: Apellidos
Fi: DNI/NIF/NIE
Birth: Fecha de nacimiento
Code: Código de trabajador
Province: Provincia
City: Población
ProfileType: Tipo de perfil
Street: Dirección
Postcode: Código postal
Web user: Usuario Web
Access permission: Permiso de acceso

View File

@ -4,6 +4,7 @@ import './main';
import './index/';
import './summary';
import './card';
import './create';
import './descriptor';
import './descriptor-popover';
import './search-panel';

View File

@ -42,6 +42,14 @@
</div>
</vn-card>
</vn-data-viewer>
<a ui-sref="worker.create"
vn-tooltip="New worker"
vn-bind="+"
vn-acl="hr"
vn-acl-action="remove"
fixed-bottom-right>
<vn-float-button icon="person_add"></vn-float-button>
</a>
<vn-popup vn-id="preview">
<vn-worker-summary
worker="$ctrl.selectedWorker">

View File

@ -0,0 +1 @@
New worker: Nuevo trabajador

View File

@ -134,6 +134,13 @@
"worker": "$ctrl.worker"
},
"acl": ["hr"]
},
{
"url": "/create",
"state": "worker.create",
"component": "vn-worker-create",
"description": "New worker",
"acl": ["hr"]
}
]
}

View File

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

3588
print/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ module.exports = {
},
props: {
url: {
type: [String],
type: String,
required: true
}
}

View File

@ -1,5 +1,5 @@
subject: Your agricultural invoice
title: Your agricultural invoice
subject: Your agricultural receipt
title: Your agricultural receipt
dear: Dear supplier
description: Attached you can find agricultural receipt generated from your last deliveries. Please return a signed and stamped copy to our administration department.
conclusion: Thanks for your attention!

View File

@ -1,5 +1,5 @@
subject: Tu factura agrícola
title: Tu factura agrícola
subject: Tu recibo agrícola
title: Tu recibo agrícola
dear: Estimado proveedor
description: Adjunto puede encontrar recibo agrícola generado de sus últimas entregas. Por favor, devuelva una copia firmada y sellada a nuestro de departamento de administración.
conclusion: ¡Gracias por tu atención!

View File

@ -1,5 +1,5 @@
subject: Votre facture agricole
title: Votre facture agricole
subject: Votre reçu agricole
title: Votre reçu agricole
dear: Cher Fournisseur
description: Vous trouverez en pièce jointe le reçu agricole généré à partir de vos dernières livraisons. Veuillez retourner une copie signée et tamponnée à notre service administratif.
conclusion: Merci pour votre attention!

View File

@ -1,5 +1,5 @@
subject: A sua fatura agrícola
title: A sua fatura agrícola
subject: A sua recibo agrícola
title: A sua recibo agrícola
dear: Caro Fornecedor
description: Em anexo encontra-se o recibo agrícola gerado a partir das suas últimas entregas. Por favor, devolva uma cópia assinada e carimbada ao nosso departamento de administração.
conclusion: Obrigado pela atenção.

View File

@ -8,8 +8,8 @@ description:
<a href='https://www.youtube.com/watch?v=qhb0kgQF3o8' title='Youtube' target='_blank'
style='color:#8dba25'>https://www.youtube.com/watch?v=qhb0kgQF3o8</a>. También
necesitarás el QLabel, el programa para imprimir las cintas.
downloadFrom: Puedes descargarlo desde este enlace <a href='https://godex.s3-accelerate.amazonaws.com/gGnOPoojkP6vC1lgmrbEqQ.file?v01'
title='Descargar QLabel' target='_blank' style='color:#8dba25'>https://godex.s3-accelerate.amazonaws.com/gGnOPoojkP6vC1lgmrbEqQ.file?v01</a>
downloadFrom: Puedes descargarlo desde este enlace <a href='https://cdn.verdnatura.es/public/QLabel_IV_V1.37_Install_en.exe'
title='Descargar QLabel' target='_blank' style='color:#8dba25'>https://cdn.verdnatura.es/public/QLabel_IV_V1.37_Install_en.exe</a>
downloadDriver: En este enlace puedes descargar el driver de la impresora <a href='https://es.seagullscientific.com/support/downloads/drivers/godex/download/'
title='Descargar driver' target='_blank' style='color:#8dba25'>https://es.seagullscientific.com/support/downloads/drivers/godex/download/</a>
sections:

View File

@ -10,7 +10,7 @@ module.exports = {
},
props: {
url: {
type: [String],
type: String,
required: true
}
}

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,8 @@
subject: Bienvenido a Verdnatura
title: "¡Te damos la bienvenida!"
dearWorker: Estimado trabajador
workerData: 'Estos son los datos de tu usuario de Verdnatura.
Usuario: <strong>{0}</strong>. Haz click aquí para
<a href="{1}"
title="Cambiar contraseña" target="_blank" style="color: #8dba25">establecer tu contraseña
</a>.'

View File

@ -0,0 +1,7 @@
SELECT
u.id,
u.name,
e.email
FROM account.user u
LEFT JOIN account.emailUser e ON e.userFk = u.id
WHERE u.id = ?;

View File

@ -0,0 +1,9 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title', [id]) }}</h1>
<p>{{ $t('dearWorker') }},</p>
<p v-html="$t('workerData', [this.worker.name, this.url])"></p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,27 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'worker-welcome',
async serverPrefetch() {
this.worker = await this.fetchWorker(this.id);
},
methods: {
fetchWorker(id) {
return this.findOneFromDef('worker', [id]);
},
},
components: {
'email-body': emailBody.build(),
},
props: {
id: {
type: Number,
required: true
},
url: {
type: String,
required: true
}
}
};

View File

@ -3,9 +3,12 @@ SELECT
i.name,
i.stems,
i.size,
b.packing
b.packing,
p.name as 'producer'
FROM vn.item i
JOIN cache.last_buy clb ON clb.item_id = i.id
JOIN vn.buy b ON b.id = clb.buy_id
JOIN vn.entry e ON e.id = b.entryFk
JOIN vn.producer p ON p.id = i.producerFk
WHERE i.id = ? AND clb.warehouse_id = ?

View File

@ -0,0 +1,12 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,85 @@
* {
box-sizing: border-box;
padding-right: 1%;
}
.label {
font-size: 1.2em;
font-family: Arial, Helvetica, sans-serif;
}
.barcode {
float: left;
width: 40%;
}
.barcode h1 {
text-align: center;
font-size: 1.8em;
margin: 0 0 10px 0
}
.barcode .image {
text-align: center
}
.barcode .image img {
width: 170px
}
.data {
float: left;
width: 60%;
}
.data .header {
background-color: #000;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin-bottom: 25px;
text-align: right;
font-size: 1.2em;
padding: 0.2em;
color: #FFF
}
.data .sector,
.data .producer {
text-transform: uppercase;
text-align: right;
font-size: 1.5em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.data .sector-sm {
text-transform: uppercase;
text-align: right;
font-size: 1.2em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.data .producer {
text-justify: inter-character;
}
.data .details {
border-top: 4px solid #000;
padding-top: 2px;
}
.data .details .package {
padding-right: 5px;
float: left;
width: 50%;
}
.package .packing,
.package .dated,
.package .labelNumber {
text-align: right
}
.package .packing {
font-size: 1.8em;
font-weight: 400
}
.data .details .size {
background-color: #000;
text-align: center;
font-size: 3em;
padding: 0.2em 0;
float: left;
width: 50%;
color: #FFF
}

View File

@ -0,0 +1,2 @@
previous: PREVIOUS
report: Report

View File

@ -0,0 +1,2 @@
previous: PREVIA
report: Ticket

View File

@ -0,0 +1,11 @@
{
"width": "10.4cm",
"height": "4.8cm",
"margin": {
"top": "0cm",
"right": "0cm",
"bottom": "0cm",
"left": "0cm"
},
"printBackground": true
}

View File

@ -0,0 +1,26 @@
<DOCTYPE html>
<body>
<div class="label">
<div class="barcode">
<h1>{{previa.saleGroupFk}}</h1>
<div class="image">
<img v-bind:src="barcode" />
</div>
</div>
<div class="data">
<div class="header">{{ $t('previous') }}</div>
<div v-if="sector.description.length > 16" class="sector-sm">
{{sector.description}}
</div>
<div v-else class="sector">{{sector.description}}</div>
<div class="producer">{{ $t('report') }}#{{previa.ticketFk}}</div>
<div class="details">
<div class="package">
<div class="packing">{{previa.itemPackingTypeFk}}</div>
<div class="dated">{{previa.shippingHour}}:{{previa.shippingMinute}}</div>
</div>
<div class="size">{{previa.items}}</div>
</div>
</div>
</div>
</body>

View File

@ -0,0 +1,42 @@
const Component = require(`vn-print/core/component`);
const reportBody = new Component('report-body');
const qrcode = require('qrcode');
const UserError = require('vn-loopback/util/user-error');
module.exports = {
name: 'previa-label',
async serverPrefetch() {
this.previa = await this.fetchPrevia(this.id);
this.sector = await this.fetchSector(this.id);
this.barcode = await this.getBarcodeBase64(this.id);
if (this.previa)
this.previa = this.previa[0];
if (!this.sector)
throw new UserError('Something went wrong - no sector found');
},
methods: {
fetchPrevia(id) {
return this.findOneFromDef('previa', [id]);
},
getBarcodeBase64(id) {
const data = String(id);
return qrcode.toDataURL(data, {margin: 0});
},
fetchSector(id) {
return this.findOneFromDef('sector', [id]);
}
},
components: {
'report-body': reportBody.build()
},
props: {
id: {
type: Number,
required: true,
description: 'The saleGroupFk id'
},
}
};

View File

@ -0,0 +1 @@
CALL vn.previousSticker_get(?)

View File

@ -0,0 +1,4 @@
SELECT s.description
FROM vn.saleGroup sg
JOIN vn.sector s ON sg.sectorFk = s.id
WHERE sg.id = ?