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

This commit is contained in:
Pablo Natek 2023-10-19 15:05:21 +02:00
commit ecbc2fb7fc
117 changed files with 1478 additions and 488 deletions

View File

@ -18,6 +18,7 @@ describe('setSaleQuantity()', () => {
it('should change quantity sale', async() => { it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
try { try {
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -0,0 +1,40 @@
module.exports = function(Self) {
Self.remoteMethod('getByUser', {
description: 'returns the starred modules for the current user',
accessType: 'READ',
accepts: [{
arg: 'userId',
type: 'number',
description: 'The user id',
required: true,
http: {source: 'path'}
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:userId/get-by-user`,
verb: 'GET'
}
});
Self.getByUser = async userId => {
const models = Self.app.models;
const appNames = ['hedera'];
const filter = {
fields: ['appName', 'url'],
where: {
appName: {inq: appNames},
environment: process.env.NODE_ENV ?? 'development',
}
};
const isWorker = await models.Account.findById(userId, {fields: ['id']});
if (!isWorker)
return models.Url.find(filter);
appNames.push('salix');
return models.Url.find(filter);
};
};

View File

@ -0,0 +1,19 @@
const {models} = require('vn-loopback/server/server');
describe('getByUser()', () => {
const worker = 1;
const notWorker = 2;
it(`should return only hedera url if not is worker`, async() => {
const urls = await models.Url.getByUser(notWorker);
expect(urls.length).toEqual(1);
expect(urls[0].appName).toEqual('hedera');
});
it(`should return more than hedera url`, async() => {
const urls = await models.Url.getByUser(worker);
expect(urls.length).toBeGreaterThan(1);
expect(urls.find(url => url.appName == 'salix').appName).toEqual('salix');
});
});

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('updateUser', {
description: 'Update user data',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The user id',
required: true,
http: {source: 'path'}
}, {
arg: 'name',
type: 'string',
description: 'The user name',
}, {
arg: 'nickname',
type: 'string',
description: 'The user nickname',
}, {
arg: 'email',
type: 'string',
description: 'The user email'
}, {
arg: 'lang',
type: 'string',
description: 'The user lang'
}
],
http: {
path: `/:id/update-user`,
verb: 'PATCH'
}
});
Self.updateUser = async(ctx, id, name, nickname, email, lang) => {
await Self.userSecurity(ctx, id);
await Self.upsertWithWhere({id}, {name, nickname, email, lang});
};
};

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('loopback model VnUser', () => { describe('loopback model VnUser', () => {
it('should return true if the user has the given role', async() => { it('should return true if the user has the given role', async() => {
@ -12,4 +13,42 @@ describe('loopback model VnUser', () => {
expect(result).toBeFalsy(); expect(result).toBeFalsy();
}); });
describe('userSecurity', () => {
const itManagementId = 115;
const hrId = 37;
const employeeId = 1;
it('should check if you are the same user', async() => {
const ctx = {options: {accessToken: {userId: employeeId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should check for higher privileges', async() => {
const ctx = {options: {accessToken: {userId: itManagementId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should check if you have medium privileges and the user email is not verified', async() => {
const ctx = {options: {accessToken: {userId: hrId}}};
await models.VnUser.userSecurity(ctx, employeeId);
});
it('should throw an error if you have medium privileges and the users email is verified', async() => {
const tx = await models.VnUser.beginTransaction({});
const ctx = {options: {accessToken: {userId: hrId}}};
try {
const options = {transaction: tx};
const userToUpdate = await models.VnUser.findById(1, null, options);
userToUpdate.updateAttribute('emailVerified', 1, options);
await models.VnUser.userSecurity(ctx, employeeId, options);
await tx.rollback();
} catch (error) {
await tx.rollback();
expect(error).toEqual(new ForbiddenError());
}
});
});
}); });

3
back/models/url.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/url/getByUser')(Self);
};

View File

@ -1,6 +1,7 @@
const vnModel = require('vn-loopback/common/models/vn-model'); const vnModel = require('vn-loopback/common/models/vn-model');
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = function(Self) { module.exports = function(Self) {
vnModel(Self); vnModel(Self);
@ -12,6 +13,7 @@ module.exports = function(Self) {
require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self); require('../methods/vn-user/renew-token')(Self);
require('../methods/vn-user/update-user')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create'); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@ -178,45 +180,75 @@ module.exports = function(Self) {
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword'); .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761 Self.userSecurity = async(ctx, userId, options) => {
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { const models = Self.app.models;
// if (!ctx.args || !ctx.args.data.email) return; const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
const ctxToken = {req: {accessToken}};
// const loopBackContext = LoopBackContext.getCurrentContext(); if (userId === accessToken.userId) return;
// const httpCtx = {req: loopBackContext.active};
// const httpRequest = httpCtx.req.http.req;
// const headers = httpRequest.headers;
// const origin = headers.origin;
// const url = origin.split(':');
// class Mailer { const myOptions = {};
// async send(verifyOptions, cb) { if (typeof options == 'object')
// const params = { Object.assign(myOptions, options);
// url: verifyOptions.verifyHref,
// recipient: verifyOptions.to,
// lang: ctx.req.getLocale()
// };
// const email = new Email('email-verify', params); const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
// email.send(); if (hasHigherPrivileges) return;
// cb(null, verifyOptions.to); const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions);
// } const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions);
// } if (!user.emailVerified && hasMediumPrivileges) return;
// const options = { throw new ForbiddenError();
// type: 'email', };
// to: instance.email,
// from: {}, Self.observe('after save', async ctx => {
// redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`, const instance = ctx?.instance;
// template: false, const newEmail = instance?.email;
// mailer: new Mailer, const oldEmail = ctx?.hookState?.oldInstance?.email;
// host: url[1].split('/')[2], if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return;
// port: url[2],
// protocol: url[0], const loopBackContext = LoopBackContext.getCurrentContext();
// user: Self const httpCtx = {req: loopBackContext.active};
// }; const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
// await instance.verify(options); const origin = headers.origin;
// }); const url = origin.split(':');
const env = process.env.NODE_ENV;
const liliumUrl = await Self.app.models.Url.findOne({
where: {and: [
{appName: 'lilium'},
{environment: env}
]}
});
class Mailer {
async send(verifyOptions, cb) {
const params = {
url: verifyOptions.verifyHref,
recipient: verifyOptions.to
};
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
}
}
const options = {
type: 'email',
to: newEmail,
from: {},
redirect: `${liliumUrl.url}verifyEmail?userId=${instance.id}`,
template: false,
mailer: new Mailer,
host: url[1].split('/')[2],
port: url[2],
protocol: url[0],
user: Self
};
await instance.verify(options, ctx.options);
});
}; };

View File

@ -18,14 +18,7 @@
"required": true "required": true
}, },
"username": { "username": {
"type": "string", "type": "string"
"mysql": {
"columnName": "name"
}
},
"password": {
"type": "string",
"required": true
}, },
"roleFk": { "roleFk": {
"type": "number", "type": "number",
@ -45,6 +38,9 @@
"email": { "email": {
"type": "string" "type": "string"
}, },
"emailVerified": {
"type": "boolean"
},
"created": { "created": {
"type": "date" "type": "date"
}, },
@ -144,7 +140,8 @@
"image", "image",
"hasGrant", "hasGrant",
"realm", "realm",
"email" "email",
"emailVerified"
] ]
} }
} }

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('WorkerDepartment', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,7 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'higherPrivileges', '*', 'ALLOW', 'ROLE', 'itManagement'),
('VnUser', 'mediumPrivileges', '*', 'ALLOW', 'ROLE', 'hr'),
('VnUser', 'updateUser', '*', 'ALLOW', 'ROLE', 'employee');
ALTER TABLE `account`.`user` ADD `username` varchar(30) AS (name) VIRTUAL;

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId)
VALUES('TicketCollection', '*', 'WRITE', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('Worker','setPassword','*','ALLOW','ROLE','employee');

View File

@ -0,0 +1,7 @@
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('hedera', 'test', 'https://test-shop.verdnatura.es/'),
('hedera', 'production', 'https://shop.verdnatura.es/');
INSERT INTO `salix`.`ACL` ( model, property, accessType, permission, principalType, principalId)
VALUES('Url', 'getByUser', 'READ', 'ALLOW', 'ROLE', '$everyone');

View File

@ -0,0 +1,6 @@
UPDATE `vn`.`supplier`
SET account = LPAD(id,10,'0')
WHERE account IS NULL;
ALTER TABLE `vn`.`supplier` ADD CONSTRAINT supplierAccountTooShort CHECK (LENGTH(account) = 10);
ALTER TABLE `vn`.`supplier` MODIFY COLUMN account varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT 4100000000 NOT NULL COMMENT 'Default accounting code for suppliers.';

View File

@ -0,0 +1,26 @@
ALTER TABLE `vn`.`zoneIncluded`
ADD COLUMN `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT FIRST,
DROP PRIMARY KEY,
DROP FOREIGN KEY `zoneFk2`,
DROP FOREIGN KEY `zoneGeoFk2`,
DROP KEY `geoFk_idx`,
ADD PRIMARY KEY (`id`),
ADD CONSTRAINT `zoneIncluded_FK_1` FOREIGN KEY (zoneFk) REFERENCES `vn`.`zone`(id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `zoneIncluded_FK_2` FOREIGN KEY (geoFk) REFERENCES `vn`.`zoneGeo`(id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `unique_zone_geo` UNIQUE (`zoneFk`, `geoFk`);
DROP TRIGGER IF EXISTS `vn`.`zoneIncluded_afterDelete`;
USE `vn`;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`zoneIncluded_afterDelete`
AFTER DELETE ON `zoneIncluded`
FOR EACH ROW
BEGIN
INSERT INTO zoneLog
SET `action` = 'delete',
`changedModel` = 'zoneIncluded',
`changedModelId` = OLD.zoneFk,
`userFk` = account.myUser_getId();
END$$
DELIMITER ;

View File

@ -2832,24 +2832,24 @@ UPDATE `account`.`user`
INSERT INTO `vn`.`ticketLog` (`originFk`, userFk, `action`, changedModel, oldInstance, newInstance, changedModelId, `description`) INSERT INTO `vn`.`ticketLog` (`originFk`, userFk, `action`, changedModel, oldInstance, newInstance, changedModelId, `description`)
VALUES VALUES
(7, 18, 'update', 'Sale', '{"quantity":1}', '{"quantity":10}', 1, NULL), (7, 18, 'update', 'Sale', '{"quantity":1}', '{"quantity":10}', 22, NULL),
(7, 18, 'update', 'Ticket', '{"quantity":1,"concept":"Chest ammo box"}', '{"quantity":10,"concept":"Chest ammo box"}', 1, NULL), (7, 18, 'update', 'Ticket', '{"quantity":1,"concept":"Chest ammo box"}', '{"quantity":10,"concept":"Chest ammo box"}', 22, NULL),
(7, 18, 'update', 'Sale', '{"price":3}', '{"price":5}', 1, NULL), (7, 18, 'update', 'Sale', '{"price":3}', '{"price":5}', 22, NULL),
(7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 100cm de '5' a '10'"), (7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 100cm de '5' a '10'"),
(16, 9, 'update', 'Sale', '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', '{"quantity":8,"concept":"Shield", "price": 10.5, "itemFk": 1}' , 5689, 'Shield'); (16, 9, 'update', 'Sale', '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', '{"quantity":8,"concept":"Shield", "price": 10.5, "itemFk": 1}' , 12, 'Shield');
INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedModel, changedModelId, changedModelValue, oldInstance, newInstance, description) INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedModel, changedModelId, changedModelValue, oldInstance, newInstance, description)
VALUES VALUES
(1, NULL, 'delete', '2001-06-09 11:00:04', 'Ticket', 45, 'Spider Man' , NULL, NULL, NULL), (1, NULL, 'delete', '2001-06-09 11:00:04', 'Ticket', 45, 'Spider Man' , NULL, NULL, NULL),
(1, 18, 'select', '2001-06-09 11:00:03', 'Ticket', 45, 'Spider Man' , NULL, NULL, NULL), (1, 18, 'select', '2001-06-09 11:00:03', 'Ticket', 45, 'Spider Man' , NULL, NULL, NULL),
(1, NULL, 'update', '2001-05-09 10:00:02', 'Sale', 69854, 'Armor' , '{"isPicked": false}','{"isPicked": true}', NULL), (1, NULL, 'update', '2001-05-09 10:00:02', 'Sale', 5, 'Armor' , '{"isPicked": false}','{"isPicked": true}', NULL),
(1, 18, 'update', '2001-01-01 10:05:01', 'Sale', 69854, 'Armor' , NULL, NULL, 'Armor quantity changed from ''15'' to ''10'''), (1, 18, 'update', '2001-01-01 10:05:01', 'Sale', 5, 'Armor' , NULL, NULL, 'Armor quantity changed from ''15'' to ''10'''),
(1, NULL, 'delete', '2001-01-01 10:00:10', 'Sale', 5689, 'Shield' , '{"quantity":10,"concept":"Shield"}', NULL, NULL), (1, NULL, 'delete', '2001-01-01 10:00:10', 'Sale', 4, 'Shield' , '{"quantity":10,"concept":"Shield"}', NULL, NULL),
(1, 18, 'insert', '2000-12-31 15:00:05', 'Sale', 69854, 'Armor' , NULL,'{"quantity":15,"concept":"Armor", "price": 345.99, "itemFk": 2}', NULL), (1, 18, 'insert', '2000-12-31 15:00:05', 'Sale', 1, 'Armor' , NULL,'{"quantity":15,"concept":"Armor", "price": 345.99, "itemFk": 2}', NULL),
(1, 18, 'update', '2000-12-28 08:40:45', 'Ticket', 45, 'Spider Man' , '{"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"isBlocked":false,"hasPriority":false,"companyFk":442,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}','{"warehouseFk":61,"shipped":"2023-05-17T22:00:00.000Z","nickname":"Spider Man","isSigned":false,"isLabeled":false,"isPrinted":false,"packages":1,"hour":0,"isBlocked":true,"hasPriority":true,"companyFk":443,"landed":"2023-05-18T22:00:00.000Z","isBoxed":false,"isDeleted":false,"zoneFk":713,"zonePrice":13,"zoneBonus":1}', NULL), (1, 18, 'update', '2000-12-28 08:40:45', 'Ticket', 45, 'Spider Man' , '{"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"isBlocked":false,"hasPriority":false,"companyFk":442,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}','{"warehouseFk":61,"shipped":"2023-05-17T22:00:00.000Z","nickname":"Spider Man","isSigned":false,"isLabeled":false,"isPrinted":false,"packages":1,"hour":0,"isBlocked":true,"hasPriority":true,"companyFk":443,"landed":"2023-05-18T22:00:00.000Z","isBoxed":false,"isDeleted":false,"zoneFk":713,"zonePrice":13,"zoneBonus":1}', NULL),
(1, 18, 'select', '2000-12-27 03:40:30', 'Ticket', 45, NULL , NULL, NULL, NULL), (1, 18, 'select', '2000-12-27 03:40:30', 'Ticket', 45, NULL , NULL, NULL, NULL),
(1, 18, 'insert', '2000-04-10 09:40:15', 'Sale', 5689, 'Shield' , NULL, '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', NULL), (1, 18, 'insert', '2000-04-10 09:40:15', 'Sale', 4, 'Shield' , NULL, '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', NULL),
(1, 18, 'insert', '1999-05-09 10:00:00', 'Ticket', 45, 'Super Man' , NULL, '{"id":45,"clientFk":8608,"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","addressFk":48637,"isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"created":"2023-05-16T11:42:56.000Z","isBlocked":false,"hasPriority":false,"companyFk":442,"agencyModeFk":639,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}', NULL); (1, 18, 'insert', '1999-05-09 10:00:00', 'Ticket', 45, 'Super Man' , NULL, '{"id":45,"clientFk":8608,"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","addressFk":48637,"isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"created":"2023-05-16T11:42:56.000Z","isBlocked":false,"hasPriority":false,"companyFk":442,"agencyModeFk":639,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}', NULL);
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`) INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES VALUES
@ -2867,6 +2867,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES VALUES
('lilium', 'development', 'http://localhost:9000/#/'), ('lilium', 'development', 'http://localhost:9000/#/'),
('hedera', 'development', 'http://localhost:9090/'),
('salix', 'development', 'http://localhost:5000/#!/'); ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)

View File

@ -30434,6 +30434,7 @@ CREATE TABLE `item` (
`editorFk` int(10) unsigned DEFAULT NULL, `editorFk` int(10) unsigned DEFAULT NULL,
`recycledPlastic` int(11) DEFAULT NULL, `recycledPlastic` int(11) DEFAULT NULL,
`nonRecycledPlastic` int(11) DEFAULT NULL, `nonRecycledPlastic` int(11) DEFAULT NULL,
`minQuantity` int(10) unsigned DEFAULT NULL COMMENT 'Cantidad mínima para una línea de venta',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`), UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`),
KEY `Color` (`inkFk`), KEY `Color` (`inkFk`),

View File

@ -39,6 +39,7 @@ import './range';
import './input-time'; import './input-time';
import './input-file'; import './input-file';
import './label'; import './label';
import './link-phone';
import './list'; import './list';
import './popover'; import './popover';
import './popup'; import './popup';

View File

@ -0,0 +1,14 @@
<span ng-if="$ctrl.phoneNumber">
{{$ctrl.phoneNumber}}
<a href="tel:{{$ctrl.phoneNumber}}">
<vn-icon
flat
round
icon="phone"
title="MicroSIP"
ng-click="$event.stopPropagation();"
>
</vn-icon>
</a>
</span>
<span ng-if="!$ctrl.phoneNumber">-</span>

View File

@ -0,0 +1,15 @@
import ngModule from '../../module';
import './style.scss';
class Controller {
constructor() {
this.phoneNumber = null;
}
}
ngModule.vnComponent('vnLinkPhone', {
template: require('./index.html'),
controller: Controller,
bindings: {
phoneNumber: '<',
}
});

View File

@ -0,0 +1,7 @@
vn-link-phone {
vn-icon {
font-size: 1.1em;
vertical-align: bottom;
}
}

View File

@ -22,5 +22,4 @@ import './user-photo';
import './upload-photo'; import './upload-photo';
import './bank-entity'; import './bank-entity';
import './log'; import './log';
import './instance-log';
import './sendSms'; import './sendSms';

View File

@ -1,12 +0,0 @@
<vn-dialog
vn-id="instanceLog">
<tpl-body>
<vn-log
class="vn-instance-log"
url="{{$ctrl.url}}"
origin-id="$ctrl.originId"
changed-model="$ctrl.changedModel"
changed-model-id="$ctrl.changedModelId">
</vn-log>
</tpl-body>
</vn-dialog>

View File

@ -1,21 +0,0 @@
import ngModule from '../../module';
import Section from '../section';
import './style.scss';
export default class Controller extends Section {
open() {
this.$.instanceLog.show();
}
}
ngModule.vnComponent('vnInstanceLog', {
controller: Controller,
template: require('./index.html'),
bindings: {
model: '<',
originId: '<',
changedModelId: '<',
changedModel: '@',
url: '@'
}
});

View File

@ -1,9 +0,0 @@
vn-log.vn-instance-log {
vn-card {
width: 900px;
visibility: hidden;
& > * {
visibility: visible;
}
}
}

View File

@ -72,6 +72,7 @@ export default class Controller extends Section {
$postLink() { $postLink() {
this.resetFilter(); this.resetFilter();
this.defaultFilter();
this.$.$watch( this.$.$watch(
() => this.$.filter, () => this.$.filter,
() => this.applyFilter(), () => this.applyFilter(),
@ -79,6 +80,14 @@ export default class Controller extends Section {
); );
} }
defaultFilter() {
const defaultFilters = ['changedModel', 'changedModelId'];
for (const defaultFilter of defaultFilters) {
if (this[defaultFilter])
this.$.filter[defaultFilter] = this[defaultFilter];
}
}
get logs() { get logs() {
return this._logs; return this._logs;
} }

View File

@ -189,5 +189,6 @@
"The sales do not exists": "The sales do not exists", "The sales do not exists": "The sales do not exists",
"Ticket without Route": "Ticket without route", "Ticket without Route": "Ticket without route",
"Booking completed": "Booking complete", "Booking completed": "Booking complete",
"The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation" "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
"You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets"
} }

View File

@ -216,6 +216,7 @@
"The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día", "The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día",
"The worker has a marked absence that day": "El trabajador tiene marcada una ausencia ese día", "The worker has a marked absence that day": "El trabajador tiene marcada una ausencia ese día",
"You can not modify is pay method checked": "No se puede modificar el campo método de pago validado", "You can not modify is pay method checked": "No se puede modificar el campo método de pago validado",
"The account size must be exactly 10 characters": "El tamaño de la cuenta debe ser exactamente de 10 caracteres",
"Can't transfer claimed sales": "No puedes transferir lineas reclamadas", "Can't transfer claimed sales": "No puedes transferir lineas reclamadas",
"You don't have privileges to create refund": "No tienes permisos para crear un abono", "You don't have privileges to create refund": "No tienes permisos para crear un abono",
"The item is required": "El artículo es requerido", "The item is required": "El artículo es requerido",
@ -319,5 +320,7 @@
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta", "Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación" "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina"
} }

View File

@ -21,5 +21,16 @@
"model": "VnUser", "model": "VnUser",
"foreignKey": "account" "foreignKey": "account"
} }
} },
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}, {
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
}]
} }

View File

@ -1,9 +1,9 @@
<mg-ajax path="VnUsers/{{patch.params.id}}/update-user" options="vnPatch"></mg-ajax>
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="VnUsers"
data="$ctrl.user" data="$ctrl.user"
id-value="$ctrl.$params.id" form="form"
form="form"> save="patch">
</vn-watcher> </vn-watcher>
<form <form
name="form" name="form"

View File

@ -18,5 +18,8 @@ ngModule.component('vnUserBasicData', {
controller: Controller, controller: Controller,
require: { require: {
card: '^vnUserCard' card: '^vnUserCard'
},
bindings: {
user: '<'
} }
}); });

View File

@ -78,7 +78,9 @@
"state": "account.card.basicData", "state": "account.card.basicData",
"component": "vn-user-basic-data", "component": "vn-user-basic-data",
"description": "Basic data", "description": "Basic data",
"acl": ["itManagement"] "params": {
"user": "$ctrl.user"
}
}, },
{ {
"url" : "/log", "url" : "/log",

View File

@ -64,7 +64,7 @@ describe('claim regularizeClaim()', () => {
claimEnds = await importTicket(ticketId, claimId, userId, options); claimEnds = await importTicket(ticketId, claimId, userId, options);
for (claimEnd of claimEnds) for (const claimEnd of claimEnds)
await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options); await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options);
let claimBefore = await models.Claim.findById(claimId, null, options); let claimBefore = await models.Claim.findById(claimId, null, options);

View File

@ -17,6 +17,7 @@ Search claim by id or client name: Buscar reclamaciones por identificador o nomb
Claim deleted!: Reclamación eliminada! Claim deleted!: Reclamación eliminada!
claim: reclamación claim: reclamación
Photos: Fotos Photos: Fotos
Development: Trazabilidad
Go to the claim: Ir a la reclamación Go to the claim: Ir a la reclamación
Sale tracking: Líneas preparadas Sale tracking: Líneas preparadas
Ticket tracking: Estados del ticket Ticket tracking: Estados del ticket

View File

@ -33,6 +33,12 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'The company id', description: 'The company id',
required: true required: true
},
{
arg: 'addressId',
type: 'number',
description: 'The address id',
required: true
} }
], ],
returns: { returns: {

View File

@ -21,6 +21,12 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'The company id', description: 'The company id',
required: true required: true
},
{
arg: 'addressId',
type: 'number',
description: 'The address id',
required: true
} }
], ],
returns: [ returns: [

View File

@ -21,6 +21,12 @@ module.exports = Self => {
type: 'number', type: 'number',
description: 'The company id', description: 'The company id',
required: true required: true
},
{
arg: 'addressId',
type: 'number',
description: 'The address id',
required: true
} }
], ],
returns: [ returns: [

View File

@ -1,15 +1,24 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('Client transactions', () => { describe('Client transactions', () => {
const ctx = {};
it('should call transactions() method to receive a list of Web Payments from BRUCE WAYNE', async() => { it('should call transactions() method to receive a list of Web Payments from BRUCE WAYNE', async() => {
const tx = await models.Client.beginTransaction({}); const tx = await models.Client.beginTransaction({});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {};
const filter = {where: {clientFk: 1101}}; const filter = {where: {clientFk: 1101}};
const result = await models.Client.transactions(ctx, filter, options); const result = await models.Client.transactions(
ctx,
filter,
undefined,
undefined,
undefined,
undefined,
undefined,
options
);
expect(result[1].id).toBeTruthy(); expect(result[1].id).toBeTruthy();
@ -26,9 +35,17 @@ describe('Client transactions', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {args: {orderFk: 6}};
const filter = {}; const filter = {};
const result = await models.Client.transactions(ctx, filter, options); const result = await models.Client.transactions(
ctx,
filter,
6,
undefined,
undefined,
undefined,
undefined,
options
);
const firstRow = result[0]; const firstRow = result[0];
@ -47,10 +64,17 @@ describe('Client transactions', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {args: {amount: 40}};
const filter = {}; const filter = {};
const result = await models.Client.transactions(ctx, filter, options); const result = await models.Client.transactions(
ctx,
filter,
undefined,
undefined,
40,
undefined,
undefined,
options
);
const randomIndex = Math.floor(Math.random() * result.length); const randomIndex = Math.floor(Math.random() * result.length);
const transaction = result[randomIndex]; const transaction = result[randomIndex];
@ -63,4 +87,43 @@ describe('Client transactions', () => {
throw e; throw e;
} }
}); });
it('should call transactions() method filtering by date', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const filter = {};
const withResults = await models.Client.transactions(
ctx,
filter,
undefined,
undefined,
undefined,
'2000/12/31',
undefined,
options
);
expect(withResults.length).toEqual(6);
const noResults = await models.Client.transactions(
ctx,
filter,
undefined,
undefined,
undefined,
'2099/12/31',
undefined,
options
);
expect(noResults.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });

View File

@ -12,22 +12,26 @@ module.exports = Self => {
arg: 'filter', arg: 'filter',
type: 'object', type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}, },
{ {
arg: 'orderFk', arg: 'orderFk',
type: 'number', type: 'number',
http: {source: 'query'}
}, },
{ {
arg: 'clientFk', arg: 'clientFk',
type: 'number', type: 'number',
http: {source: 'query'}
}, },
{ {
arg: 'amount', arg: 'amount',
type: 'number', type: 'number',
http: {source: 'query'} },
{
arg: 'from',
type: 'date',
},
{
arg: 'to',
type: 'date',
} }
], ],
returns: { returns: {
@ -40,14 +44,15 @@ module.exports = Self => {
} }
}); });
Self.transactions = async(ctx, filter, options) => { Self.transactions = async(ctx, filter, orderFk, clientFk, amount, from, to, options) => {
const args = ctx.args;
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const where = buildFilter(args, (param, value) => { if (to) to.setHours(23, 59, 59, 999);
const where = buildFilter({orderFk, clientFk, amount, from, to}, (param, value) => {
switch (param) { switch (param) {
case 'orderFk': case 'orderFk':
return {'t.id': value}; return {'t.id': value};
@ -55,6 +60,10 @@ module.exports = Self => {
return {'t.clientFk': value}; return {'t.clientFk': value};
case 'amount': case 'amount':
return {'t.amount': (value * 100)}; return {'t.amount': (value * 100)};
case 'from':
return {'t.created': {gte: value}};
case 'to':
return {'t.created': {lte: value}};
} }
}); });

View File

@ -13,6 +13,7 @@
'description', 'description',
'model', 'model',
'hasCompany', 'hasCompany',
'hasAddress',
'hasPreview', 'hasPreview',
'datepickerEnabled' 'datepickerEnabled'
]" ]"
@ -69,6 +70,32 @@
ng-if="sampleType.selection.hasCompany" ng-if="sampleType.selection.hasCompany"
required="true"> required="true">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete
ng-if="sampleType.selection.id == 20"
vn-one
required="true"
data="$ctrl.addresses"
label="Address"
show-field="nickname"
value-field="id"
ng-model="$ctrl.addressId"
model="ClientSample.addressFk"
order="isActive DESC">
<tpl-item class="address" ng-class="::{inactive: !isActive}">
<span class="inactive" translate>{{::!isActive ? '(Inactive)' : ''}}</span>
{{::nickname}}
<span ng-show="city || province || street">
, {{::street}}, {{::city}}, {{::province.name}} - {{::agencyMode.name}}
</span>
</tpl-item>
<append>
<vn-icon-button
ui-sref="client.card.address.edit({id: $ctrl.clientId, addressId: $ctrl.addressId})"
icon="edit"
vn-tooltip="Edit address">
</vn-icon-button>
</append>
</vn-autocomplete>
<vn-date-picker <vn-date-picker
vn-one vn-one
label="From" label="From"

View File

@ -15,8 +15,10 @@ class Controller extends Section {
set client(value) { set client(value) {
this._client = value; this._client = value;
if (value) if (value) {
this.setClientSample(value); this.setClientSample(value);
this.clientAddressesList(value.id);
}
} }
get companyId() { get companyId() {
@ -29,6 +31,16 @@ class Controller extends Section {
this.clientSample.companyFk = value; this.clientSample.companyFk = value;
} }
get addressId() {
if (!this.clientSample.addressId)
this.clientSample.addressId = this.client.defaultAddressFk;
return this.clientSample.addressId;
}
set addressId(value) {
this.clientSample.addressId = value;
}
onSubmit() { onSubmit() {
this.$.watcher.check(); this.$.watcher.check();
@ -65,6 +77,9 @@ class Controller extends Section {
if (sampleType.datepickerEnabled) if (sampleType.datepickerEnabled)
params.from = this.clientSample.from; params.from = this.clientSample.from;
if (this.clientSample.addressId)
params.addressId = this.clientSample.addressId;
} }
preview() { preview() {
@ -126,6 +141,40 @@ class Controller extends Section {
}; };
}); });
} }
clientAddressesList(value) {
let filter = {
include: [
{
relation: 'province',
scope: {
fields: ['name']
}
},
{
relation: 'agencyMode',
scope: {
fields: ['name']
}
}
]
};
filter = encodeURIComponent(JSON.stringify(filter));
let query = `Clients/${value}/addresses?filter=${filter}`;
this.$http.get(query).then(res => {
if (res.data)
this.addresses = res.data;
});
}
getClientDefaultAddress(value) {
let query = `Clients/${value}`;
this.$http.get(query).then(res => {
if (res.data)
this.addressId = res.data.defaultAddressFk;
});
}
} }
Controller.$inject = ['$element', '$scope', 'vnEmail']; Controller.$inject = ['$element', '$scope', 'vnEmail'];

View File

@ -45,11 +45,18 @@
<vn-label-value label="Contact" <vn-label-value label="Contact"
value="{{$ctrl.summary.contact}}"> value="{{$ctrl.summary.contact}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Phone" <vn-label-value label="Phone">
value="{{$ctrl.summary.phone}}">
<vn-link-phone
phone-number="$ctrl.summary.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Mobile" <vn-label-value label="Mobile">
value="{{$ctrl.summary.mobile}}">
<vn-link-phone
phone-number="$ctrl.summary.mobile"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Email" no-ellipsize <vn-label-value label="Email" no-ellipsize
value="{{$ctrl.listEmails($ctrl.summary.email)}}"> value="{{$ctrl.listEmails($ctrl.summary.email)}}">

View File

@ -47,6 +47,11 @@
"type": "belongsTo", "type": "belongsTo",
"model": "VnUser", "model": "VnUser",
"foreignKey": "userFk" "foreignKey": "userFk"
},
"shelving": {
"type": "belongsTo",
"model": "Shelving",
"foreignKey": "shelvingFk"
} }
} }
} }

View File

@ -131,6 +131,9 @@
"nonRecycledPlastic": { "nonRecycledPlastic": {
"type": "number" "type": "number"
}, },
"minQuantity": {
"type": "number"
},
"packingOut": { "packingOut": {
"type": "number" "type": "number"
}, },
@ -154,6 +157,10 @@
"mysql":{ "mysql":{
"columnName": "doPhoto" "columnName": "doPhoto"
} }
},
"minQuantity": {
"type": "number",
"description": "Min quantity"
} }
}, },
"relations": { "relations": {

View File

@ -33,6 +33,8 @@
rule rule
info="Full name calculates based on tags 1-3. Is not recommended to change it manually"> info="Full name calculates based on tags 1-3. Is not recommended to change it manually">
</vn-textfield> </vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete <vn-autocomplete
url="ItemTypes" url="ItemTypes"
label="Type" label="Type"
@ -50,6 +52,30 @@
</div> </div>
</tpl-item> </tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete
label="Generic"
url="Items/withName"
ng-model="$ctrl.item.genericFk"
vn-name="generic"
show-field="name"
value-field="id"
search-function="$ctrl.itemSearchFunc($search)"
order="id DESC"
tabindex="1">
<tpl-item>
<div>{{::name}}</div>
<div class="text-caption text-secondary">
#{{::id}}
</div>
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.item)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
@ -128,30 +154,13 @@
ng-model="$ctrl.item.stemMultiplier" ng-model="$ctrl.item.stemMultiplier"
vn-name="stemMultiplier"> vn-name="stemMultiplier">
</vn-input-number> </vn-input-number>
<vn-autocomplete <vn-input-number
label="Generic" min="1"
url="Items/withName" label="Minimum sales quantity"
ng-model="$ctrl.item.genericFk" ng-model="$ctrl.item.minQuantity"
vn-name="generic" vn-name="minQuantity"
show-field="name" rule>
value-field="id" </vn-input-number>
search-function="$ctrl.itemSearchFunc($search)"
order="id DESC"
tabindex="1">
<tpl-item>
<div>{{::name}}</div>
<div class="text-caption text-secondary">
#{{::id}}
</div>
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.item)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-input-number <vn-input-number

View File

@ -16,3 +16,4 @@ This item does need a photo: Este artículo necesita una foto
Do photo: Hacer foto Do photo: Hacer foto
Recycled Plastic: Plástico reciclado Recycled Plastic: Plástico reciclado
Non recycled plastic: Plástico no reciclado Non recycled plastic: Plástico no reciclado
Minimum sales quantity: Cantidad mínima de venta

View File

@ -128,6 +128,9 @@
<vn-label-value label="Non recycled plastic" <vn-label-value label="Non recycled plastic"
value="{{$ctrl.summary.item.nonRecycledPlastic}}"> value="{{$ctrl.summary.item.nonRecycledPlastic}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Minimum sales quantity"
value="{{$ctrl.summary.item.minQuantity}}">
</vn-label-value>
</vn-one> </vn-one>
<vn-one name="tags"> <vn-one name="tags">
<h4 ng-show="$ctrl.isBuyer || $ctrl.isReplenisher"> <h4 ng-show="$ctrl.isBuyer || $ctrl.isReplenisher">

View File

@ -2,3 +2,4 @@ Barcode: Códigos de barras
Other data: Otros datos Other data: Otros datos
Go to the item: Ir al artículo Go to the item: Ir al artículo
WarehouseFk: Calculado sobre el almacén de {{ warehouseName }} WarehouseFk: Calculado sobre el almacén de {{ warehouseName }}
Minimum sales quantity: Cantidad mínima de venta

View File

@ -100,8 +100,7 @@ module.exports = Self => {
)); ));
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT SELECT i.id,
i.id,
i.name, i.name,
i.subName, i.subName,
i.image, i.image,
@ -116,15 +115,17 @@ module.exports = Self => {
i.stars, i.stars,
tci.price, tci.price,
tci.available, tci.available,
w.lastName AS lastName, w.lastName,
w.firstName, w.firstName,
tci.priceKg, tci.priceKg,
ink.hex ink.hex,
i.minQuantity
FROM tmp.ticketCalculateItem tci FROM tmp.ticketCalculateItem tci
JOIN vn.item i ON i.id = tci.itemFk JOIN vn.item i ON i.id = tci.itemFk
JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.worker w on w.id = it.workerFk JOIN vn.worker w on w.id = it.workerFk
LEFT JOIN vn.ink ON ink.id = i.inkFk`); LEFT JOIN vn.ink ON ink.id = i.inkFk
`);
// Apply order by tag // Apply order by tag
if (orderBy.isTag) { if (orderBy.isTag) {

View File

@ -37,9 +37,24 @@
value="{{::item.value7}}"> value="{{::item.value7}}">
</vn-label-value> </vn-label-value>
</div> </div>
<vn-horizontal>
<vn-one>
<vn-rating ng-if="::item.stars" <vn-rating ng-if="::item.stars"
ng-model="::item.stars"> ng-model="::item.stars"/>
</vn-rating> </vn-one>
<vn-horizontal
class="text-right text-caption alert vn-mr-xs"
ng-if="::item.minQuantity">
<vn-one>
<vn-icon
icon="production_quantity_limits"
translate-attr="{title: 'Minimal quantity'}"
class="text-subtitle1">
</vn-icon>
</vn-one>
{{::item.minQuantity}}
</vn-horizontal>
</vn-horizontal>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<vn-one> <vn-one>

View File

@ -1 +1,2 @@
Order created: Orden creada Order created: Orden creada
Minimal quantity: Cantidad mínima

View File

@ -44,4 +44,7 @@ vn-order-catalog {
height: 30px; height: 30px;
position: relative; position: relative;
} }
.alert {
color: $color-alert;
}
} }

View File

@ -48,8 +48,10 @@
<vn-label-value label="Landed" <vn-label-value label="Landed"
value="{{$ctrl.summary.landed | date: 'dd/MM/yyyy HH:mm'}}"> value="{{$ctrl.summary.landed | date: 'dd/MM/yyyy HH:mm'}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Phone" <vn-label-value label="Phone">
value="{{$ctrl.summary.address.phone}}"> <vn-link-phone
phone-number="$ctrl.summary.address.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Created from" <vn-label-value label="Created from"
value="{{$ctrl.summary.sourceApp}}"> value="{{$ctrl.summary.sourceApp}}">

View File

@ -25,7 +25,10 @@
<vn-one> <vn-one>
<vn-label-value <vn-label-value
label="Phone" label="Phone"
value="{{summary.phone}}"> >
<vn-link-phone
phone-number="summary.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value <vn-label-value
label="Worker" label="Worker"

View File

@ -1,10 +1,9 @@
@import "variables"; @import "variables";
vn-roadmap-summary .summary { vn-roadmap-summary .summary {
a { a:not(vn-link-phone a) {
display: flex; display: flex;
align-items: center; align-items: center;
height: 18.328px; height: 18.328px;
} }
} }

View File

@ -32,6 +32,7 @@ describe('Supplier newSupplier()', () => {
const result = await models.Supplier.newSupplier(ctx, options); const result = await models.Supplier.newSupplier(ctx, options);
expect(result.name).toEqual('newSupplier'); expect(result.name).toEqual('newSupplier');
await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; throw e;

View File

@ -123,5 +123,21 @@ describe('loopback model Supplier', () => {
throw e; throw e;
} }
}); });
it('should update the account attribute when a new supplier is created', async() => {
const tx = await models.Supplier.beginTransaction({});
const options = {transaction: tx};
try {
const newSupplier = await models.Supplier.create({name: 'Alfred Pennyworth'}, options);
const fetchedSupplier = await models.Supplier.findById(newSupplier.id, null, options);
expect(Number(fetchedSupplier.account)).toEqual(4100000000 + newSupplier.id);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });
}); });

View File

@ -117,6 +117,16 @@ module.exports = Self => {
throw new UserError('You can not modify is pay method checked'); throw new UserError('You can not modify is pay method checked');
}); });
Self.observe('after save', async function(ctx) {
if (ctx.instance && ctx.isNewInstance) {
const {id} = ctx.instance;
const {Supplier} = Self.app.models;
const supplier = await Supplier.findById(id, null, ctx.options);
await supplier?.updateAttribute('account', Number(supplier.account) + id, ctx.options);
}
});
Self.validateAsync('name', 'countryFk', hasSupplierSameName, { Self.validateAsync('name', 'countryFk', hasSupplierSameName, {
message: 'A supplier with the same name already exists. Change the country.' message: 'A supplier with the same name already exists. Change the country.'
}); });

View File

@ -18,7 +18,6 @@
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit <vn-submit
disabled="!watcher.dataChanged()"
label="Create"> label="Create">
</vn-submit> </vn-submit>
<vn-button <vn-button

View File

@ -55,7 +55,7 @@ module.exports = Self => {
const refoundZoneId = refundAgencyMode.zones()[0].id; const refoundZoneId = refundAgencyMode.zones()[0].id;
if (salesIds) { if (salesIds.length) {
const salesFilter = { const salesFilter = {
where: {id: {inq: salesIds}}, where: {id: {inq: salesIds}},
include: { include: {
@ -91,16 +91,14 @@ module.exports = Self => {
await models.SaleComponent.create(components, myOptions); await models.SaleComponent.create(components, myOptions);
} }
} }
if (!refundTicket) { if (!refundTicket) {
const servicesFilter = { const servicesFilter = {
where: {id: {inq: servicesIds}} where: {id: {inq: servicesIds}}
}; };
const services = await models.TicketService.find(servicesFilter, myOptions); const services = await models.TicketService.find(servicesFilter, myOptions);
const ticketsIds = [...new Set(services.map(service => service.ticketFk))]; const firstTicketId = services[0].ticketFk;
const now = Date.vnNew(); const now = Date.vnNew();
const [firstTicketId] = ticketsIds;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions); refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
@ -114,8 +112,8 @@ module.exports = Self => {
for (const service of services) { for (const service of services) {
await models.TicketService.create({ await models.TicketService.create({
description: service.description, description: service.description,
quantity: service.quantity, quantity: - service.quantity,
price: - service.price, price: service.price,
taxClassFk: service.taxClassFk, taxClassFk: service.taxClassFk,
ticketFk: refundTicket.id, ticketFk: refundTicket.id,
ticketServiceTypeFk: service.ticketServiceTypeFk, ticketServiceTypeFk: service.ticketServiceTypeFk,

View File

@ -1,21 +1,9 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('sale updateQuantity()', () => { describe('sale updateQuantity()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: 9}, accessToken: {userId: 9},
@ -23,6 +11,18 @@ describe('sale updateQuantity()', () => {
__: () => {} __: () => {}
} }
}; };
function getActiveCtx(userId) {
return {
active: {
accessToken: {userId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
}
};
}
it('should throw an error if the quantity is greater than it should be', async() => { it('should throw an error if the quantity is greater than it should be', async() => {
const ctx = { const ctx = {
@ -32,13 +32,16 @@ describe('sale updateQuantity()', () => {
__: () => {} __: () => {}
} }
}; };
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});
let error; let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, 17, 99, options); await models.Sale.updateQuantity(ctx, 17, 31, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -50,7 +53,6 @@ describe('sale updateQuantity()', () => {
}); });
it('should add quantity if the quantity is greater than it should be and is role advanced', async() => { it('should add quantity if the quantity is greater than it should be and is role advanced', async() => {
const tx = await models.Sale.beginTransaction({});
const saleId = 17; const saleId = 17;
const buyerId = 35; const buyerId = 35;
const ctx = { const ctx = {
@ -60,6 +62,9 @@ describe('sale updateQuantity()', () => {
__: () => {} __: () => {}
} }
}; };
const tx = await models.Sale.beginTransaction({});
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(buyerId));
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
@ -87,6 +92,8 @@ describe('sale updateQuantity()', () => {
}); });
it('should update the quantity of a given sale current line', async() => { it('should update the quantity of a given sale current line', async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9));
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});
const saleId = 25; const saleId = 25;
const newQuantity = 4; const newQuantity = 4;
@ -119,6 +126,8 @@ describe('sale updateQuantity()', () => {
__: () => {} __: () => {}
} }
}; };
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
const saleId = 17; const saleId = 17;
const newQuantity = -10; const newQuantity = -10;
@ -140,6 +149,8 @@ describe('sale updateQuantity()', () => {
}); });
it('should update a negative quantity when is a ticket refund', async() => { it('should update a negative quantity when is a ticket refund', async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9));
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});
const saleId = 13; const saleId = 13;
const newQuantity = -10; const newQuantity = -10;
@ -159,4 +170,70 @@ describe('sale updateQuantity()', () => {
throw e; throw e;
} }
}); });
it('should throw an error if the quantity is less than the minimum quantity of the item', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
const tx = await models.Sale.beginTransaction({});
const itemId = 2;
const saleId = 17;
const minQuantity = 30;
const newQuantity = minQuantity - 1;
let error;
try {
const options = {transaction: tx};
const item = await models.Item.findById(itemId, null, options);
await item.updateAttribute('minQuantity', minQuantity, options);
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('The amount cannot be less than the minimum'));
});
it('should change quantity if has minimum quantity and new quantity is equal than item available', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
const tx = await models.Sale.beginTransaction({});
const itemId = 2;
const saleId = 17;
const minQuantity = 30;
const newQuantity = minQuantity - 1;
try {
const options = {transaction: tx};
const item = await models.Item.findById(itemId, null, options);
await item.updateAttribute('minQuantity', minQuantity, options);
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: newQuantity}))));
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });

View File

@ -1,4 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateQuantity', { Self.remoteMethodCtx('updateQuantity', {
@ -64,17 +63,6 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions); const sale = await models.Sale.findById(id, filter, myOptions);
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
if (newQuantity > sale.quantity && !isRoleAdvanced)
throw new UserError('The new quantity should be smaller than the old one');
const ticketRefund = await models.TicketRefund.findOne({
where: {refundTicketFk: sale.ticketFk},
fields: ['id']}
, myOptions);
if (newQuantity < 0 && !ticketRefund)
throw new UserError('You can only add negative amounts in refund tickets');
const oldQuantity = sale.quantity; const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);

View File

@ -63,17 +63,6 @@ module.exports = Self => {
} }
}, myOptions); }, myOptions);
const itemInfo = await models.Item.getVisibleAvailable(
itemId,
ticket.warehouseFk,
ticket.shipped,
myOptions
);
const isPackaging = item.family == 'EMB';
if (!isPackaging && itemInfo.available < quantity)
throw new UserError(`This item is not available`);
const newSale = await models.Sale.create({ const newSale = await models.Sale.create({
ticketFk: id, ticketFk: id,
itemFk: item.id, itemFk: item.id,

View File

@ -138,14 +138,15 @@ module.exports = Self => {
// Update original sale // Update original sale
const rest = originalSale.quantity - sale.quantity; const rest = originalSale.quantity - sale.quantity;
query = `UPDATE sale query = `UPDATE sale
SET quantity = ? SET quantity = ?,
originalQuantity = ?
WHERE id = ?`; WHERE id = ?`;
await Self.rawSql(query, [rest, sale.id], options); await Self.rawSql(query, [rest, rest, sale.id], options);
// Clone sale with new quantity // Clone sale with new quantity
query = `INSERT INTO sale (itemFk, ticketFk, concept, quantity, originalQuantity, price, discount, priceFixed, query = `INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, discount, priceFixed,
reserved, isPicked, isPriceFixed, isAdded) reserved, isPicked, isPriceFixed, isAdded)
SELECT itemFk, ?, concept, ?, originalQuantity, price, discount, priceFixed, SELECT itemFk, ?, concept, ?, price, discount, priceFixed,
reserved, isPicked, isPriceFixed, isAdded reserved, isPicked, isPriceFixed, isAdded
FROM sale FROM sale
WHERE id = ?`; WHERE id = ?`;

View File

@ -1,3 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const LoopBackContext = require('loopback-context');
module.exports = Self => { module.exports = Self => {
require('../methods/sale/getClaimableFromTicket')(Self); require('../methods/sale/getClaimableFromTicket')(Self);
require('../methods/sale/reserve')(Self); require('../methods/sale/reserve')(Self);
@ -13,4 +16,77 @@ module.exports = Self => {
Self.validatesPresenceOf('concept', { Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank` message: `Concept cannot be blank`
}); });
Self.observe('before save', async ctx => {
const models = Self.app.models;
const changes = ctx.data || ctx.instance;
const instance = ctx.currentInstance;
const newQuantity = changes?.quantity;
if (newQuantity == null) return;
const loopBackContext = LoopBackContext.getCurrentContext();
ctx.req = loopBackContext.active;
if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'canForceQuantity', 'WRITE')) return;
const ticketId = changes?.ticketFk || instance?.ticketFk;
const itemId = changes?.itemFk || instance?.itemFk;
const ticket = await models.Ticket.findById(
ticketId,
{
fields: ['id', 'clientFk', 'warehouseFk', 'shipped'],
include: {
relation: 'client',
scope: {
fields: ['id', 'clientTypeFk'],
include: {
relation: 'type',
scope: {
fields: ['code', 'description']
}
}
}
}
},
ctx.options);
if (ticket?.client()?.type()?.code === 'loses') return;
const isRefund = await models.TicketRefund.findOne({
fields: ['id'],
where: {refundTicketFk: ticketId}
}, ctx.options);
if (isRefund) return;
if (newQuantity < 0)
throw new UserError('You can only add negative amounts in refund tickets');
const item = await models.Item.findOne({
fields: ['family', 'minQuantity'],
where: {id: itemId},
}, ctx.options);
if (item.family == 'EMB') return;
const itemInfo = await models.Item.getVisibleAvailable(
itemId,
ticket.warehouseFk,
ticket.shipped,
ctx.options
);
const oldQuantity = instance?.quantity ?? null;
const quantityAdded = newQuantity - oldQuantity;
if (itemInfo.available < quantityAdded)
throw new UserError(`This item is not available`);
if (await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*')) return;
if (newQuantity < item.minQuantity && itemInfo.available != newQuantity)
throw new UserError('The amount cannot be less than the minimum');
if (!ctx.isNewInstance && newQuantity > oldQuantity)
throw new UserError('The new quantity should be smaller than the old one');
});
}; };

View File

@ -10,6 +10,9 @@
"id": { "id": {
"id": true, "id": true,
"type": "number" "type": "number"
},
"usedShelves": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -16,8 +16,8 @@ class Controller extends SearchPanel {
this.$http.get('ItemPackingTypes', {filter}).then(res => { this.$http.get('ItemPackingTypes', {filter}).then(res => {
for (let ipt of res.data) { for (let ipt of res.data) {
itemPackingTypes.push({ itemPackingTypes.push({
code: ipt.code, description: this.$t(ipt.description),
description: this.$t(ipt.description) code: ipt.code
}); });
} }
this.itemPackingTypes = itemPackingTypes; this.itemPackingTypes = itemPackingTypes;

View File

@ -163,26 +163,16 @@ export default class Controller extends Section {
return {'futureId': value}; return {'futureId': value};
case 'liters': case 'liters':
return {'liters': value}; return {'liters': value};
case 'lines':
return {'lines': value};
case 'futureLiters': case 'futureLiters':
return {'futureLiters': value}; return {'futureLiters': value};
case 'lines':
return {'lines': value};
case 'futureLines': case 'futureLines':
return {'futureLines': value}; return {'futureLines': value};
case 'ipt': case 'ipt':
return {or: return {'ipt': {like: `%${value}%`}};
[
{'ipt': {like: `%${value}%`}},
{'ipt': null}
]
};
case 'futureIpt': case 'futureIpt':
return {or: return {'futureIpt': {like: `%${value}%`}};
[
{'futureIpt': {like: `%${value}%`}},
{'futureIpt': null}
]
};
case 'totalWithVat': case 'totalWithVat':
return {'totalWithVat': value}; return {'totalWithVat': value};
case 'futureTotalWithVat': case 'futureTotalWithVat':

View File

@ -201,7 +201,7 @@ class Controller extends Section {
sendImportSms() { sendImportSms() {
const params = { const params = {
ticketId: this.id, ticketId: this.id,
created: this.ticket.updated shipped: this.ticket.shipped
}; };
this.showSMSDialog({ this.showSMSDialog({
message: this.$t('Minimum is needed', params) message: this.$t('Minimum is needed', params)

View File

@ -1,3 +1,3 @@
Make a payment: "Verdnatura communicates:\rYour order is pending of payment.\rPlease, enter the web page and make the payment with card.\rThank you." Make a payment: "Verdnatura communicates:\rYour order is pending of payment.\rPlease, enter the web page and make the payment with card.\rThank you."
Minimum is needed: "Verdnatura communicates:\rA minimum import of 50€ (Without BAT) is needed for your order {{ticketId}} from date {{created | date: 'dd/MM/yyyy'}} to receive it with no extra fees." Minimum is needed: "Verdnatura communicates:\rA minimum import of 50€ (Without BAT) is needed for your order {{ticketId}} from date {{shipped | date: 'dd/MM/yyyy'}} to receive it with no extra fees."
Send changes: "Verdnatura communicates:\rOrder {{ticketId}} date {{created | date: 'dd/MM/yyyy'}}\r{{changes}}" Send changes: "Verdnatura communicates:\rOrder {{ticketId}} date {{created | date: 'dd/MM/yyyy'}}\r{{changes}}"

View File

@ -3,8 +3,8 @@ Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
Show pallet report: Ver hoja de pallet Show pallet report: Ver hoja de pallet
Change shipped hour: Cambiar hora de envío Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío Shipped hour: Hora de envío
Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectue el pago con tarjeta.\rMuchas gracias." Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectúe el pago con tarjeta.\rMuchas gracias."
Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales." Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{shipped | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales."
Ticket invoiced: Ticket facturado Ticket invoiced: Ticket facturado
Make invoice: Crear factura Make invoice: Crear factura
Regenerate invoice PDF: Regenerar PDF factura Regenerate invoice PDF: Regenerar PDF factura

View File

@ -18,3 +18,4 @@ Multiple invoice: Factura múltiple
Make invoice...: Crear factura... Make invoice...: Crear factura...
Invoice selected tickets: Facturar tickets seleccionados Invoice selected tickets: Facturar tickets seleccionados
Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets? Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets?
Rounding: Redondeo

View File

@ -1 +1,6 @@
<vn-log url="TicketLogs" origin-id="$ctrl.$params.id"></vn-log> <vn-log
url="TicketLogs"
origin-id="$ctrl.$params.id"
changed-model="$ctrl.$params.changedModel"
changed-model-id="$ctrl.$params.changedModelId">
</vn-log>

View File

@ -187,7 +187,7 @@
} }
}, },
{ {
"url" : "/log", "url" : "/log?changedModel&changedModelId",
"state": "ticket.card.log", "state": "ticket.card.log",
"component": "vn-ticket-log", "component": "vn-ticket-log",
"description": "Log" "description": "Log"

View File

@ -212,16 +212,9 @@
vn-none vn-none
vn-tooltip="History" vn-tooltip="History"
icon="history" icon="history"
ng-click="log.open()" ng-click="$ctrl.goToLog(sale.id)"
ng-show="sale.$hasLogs"> ng-show="sale.$hasLogs">
</vn-icon-button> </vn-icon-button>
<vn-instance-log
vn-id="log"
url="TicketLogs"
origin-id="$ctrl.$params.id"
changed-model="Sale"
changed-model-id="sale.id">
</vn-instance-log>
</vn-td> </vn-td>
</vn-tr> </vn-tr>
@ -318,7 +311,7 @@
clear-disabled="true" clear-disabled="true"
suffix="%"> suffix="%">
</vn-input-number> </vn-input-number>
<vn-vertical ng-if="$ctrl.usesMana && $ctrl.currentWorkerMana != 0"> <vn-vertical ng-if="$ctrl.usesMana">
<vn-radio <vn-radio
label="Promotion mana" label="Promotion mana"
val="mana" val="mana"

View File

@ -97,14 +97,6 @@ class Controller extends Section {
}); });
}); });
this.getUsesMana(); this.getUsesMana();
this.getCurrentWorkerMana();
}
getCurrentWorkerMana() {
this.$http.get(`WorkerManas/getCurrentWorkerMana`)
.then(res => {
this.currentWorkerMana = res.data;
});
} }
getUsesMana() { getUsesMana() {
@ -558,6 +550,14 @@ class Controller extends Section {
cancel() { cancel() {
this.$.editDiscount.hide(); this.$.editDiscount.hide();
} }
goToLog(saleId) {
this.$state.go('ticket.card.log', {
originId: this.$params.id,
changedModel: 'Sale',
changedModelId: saleId
});
}
} }
ngModule.vnComponent('vnTicketSale', { ngModule.vnComponent('vnTicketSale', {

View File

@ -120,12 +120,10 @@ describe('Ticket', () => {
const expectedAmount = 250; const expectedAmount = 250;
$httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount); $httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount);
$httpBackend.expect('GET', 'Sales/usesMana').respond(200); $httpBackend.expect('GET', 'Sales/usesMana').respond(200);
$httpBackend.expect('GET', 'WorkerManas/getCurrentWorkerMana').respond(200, expectedAmount);
controller.getMana(); controller.getMana();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.edit.mana).toEqual(expectedAmount); expect(controller.edit.mana).toEqual(expectedAmount);
expect(controller.currentWorkerMana).toEqual(expectedAmount);
}); });
}); });

View File

@ -29,7 +29,7 @@
disabled="watcher.dataChanged() || !$ctrl.checkeds.length" disabled="watcher.dataChanged() || !$ctrl.checkeds.length"
label="Pay" label="Pay"
ng-click="$ctrl.createRefund()" ng-click="$ctrl.createRefund()"
vn-acl="invoicing, claimManager, salesAssistant" vn-acl="invoicing, claimManager, salesAssistant, buyer"
vn-acl-action="remove"> vn-acl-action="remove">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
@ -37,7 +37,9 @@
<vn-check <vn-check
tabindex="1" tabindex="1"
on-change="$ctrl.addChecked(service.id)" on-change="$ctrl.addChecked(service.id)"
disabled="!service.id"> disabled="!service.id"
vn-acl="invoicing, claimManager, salesAssistant, buyer"
vn-acl-action="remove">
</vn-check> </vn-check>
<vn-autocomplete vn-two vn-focus <vn-autocomplete vn-two vn-focus
data="ticketServiceTypes" data="ticketServiceTypes"
@ -68,7 +70,8 @@
vn-one vn-one
label="Price" label="Price"
ng-model="service.price" ng-model="service.price"
step="0.01"> step="0.01"
min="0">
</vn-input-number> </vn-input-number>
<vn-auto> <vn-auto>
<vn-icon-button <vn-icon-button

View File

@ -83,19 +83,29 @@
</vn-label-value> </vn-label-value>
<vn-label-value label="Address phone" <vn-label-value label="Address phone"
ng-if="$ctrl.summary.address.phone != null" ng-if="$ctrl.summary.address.phone != null"
value="{{$ctrl.summary.address.phone}}"> >
<vn-link-phone
phone-number="$ctrl.summary.address.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Address mobile" <vn-label-value label="Address mobile"
ng-if="$ctrl.summary.address.mobile != null" ng-if="$ctrl.summary.address.mobile != null"
value="{{$ctrl.summary.address.mobile}}"> >
<vn-link-phone
phone-number="$ctrl.summary.address.mobile"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Client phone" <vn-label-value label="Client phone"
ng-if="$ctrl.summary.client.phone != null" ng-if="$ctrl.summary.client.phone != null">
value="{{$ctrl.summary.client.phone}}"> <vn-link-phone
phone-number="$ctrl.summary.client.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Client mobile" <vn-label-value label="Client mobile"
ng-if="$ctrl.summary.client.mobile != null" ng-if="$ctrl.summary.client.mobile != null">
value="{{$ctrl.summary.client.mobile}}"> <vn-link-phone
phone-number="$ctrl.summary.client.mobile"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Address" no-ellipsize <vn-label-value label="Address" no-ellipsize
value="{{$ctrl.formattedAddress}}"> value="{{$ctrl.formattedAddress}}">

View File

@ -47,4 +47,9 @@ vn-ticket-summary .summary {
} }
} }
} }
vn-icon.tel {
font-size: 1.1em;
vertical-align: bottom;
}
} }

View File

@ -1,26 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('getCurrentWorkerMana', {
description: 'Returns the mana of the logged worker',
accessType: 'READ',
accepts: [],
returns: {
type: 'number',
root: true
},
http: {
path: `/getCurrentWorkerMana`,
verb: 'GET'
}
});
Self.getCurrentWorkerMana = async ctx => {
let userId = ctx.req.accessToken.userId;
let workerMana = await Self.app.models.WorkerMana.findOne({
where: {workerFk: userId},
fields: 'amount'
});
return workerMana ? workerMana.amount : 0;
};
};

View File

@ -1,15 +0,0 @@
const app = require('vn-loopback/server/server');
describe('workerMana getCurrentWorkerMana()', () => {
it('should get the mana of the logged worker', async() => {
let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 18}}});
expect(mana).toEqual(124);
});
it('should return 0 if the user doesnt uses mana', async() => {
let mana = await app.models.WorkerMana.getCurrentWorkerMana({req: {accessToken: {userId: 9}}});
expect(mana).toEqual(0);
});
});

View File

@ -66,46 +66,36 @@ module.exports = Self => {
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate');
const destroyAllWhere = {
timed: {between: [started, ended]},
isSendMail: true
};
const updateAllWhere = {
year: args.year,
week: args.week
};
const tmpUserSQL = `
CREATE OR REPLACE TEMPORARY TABLE tmp.user
SELECT id as userFk
FROM vn.worker`;
let tmpUser = new ParameterizedSQL(tmpUserSQL);
if (args.workerId) { if (args.workerId) {
await models.WorkerTimeControl.destroyAll({ destroyAllWhere.userFk = args.workerId;
userFk: args.workerId, updateAllWhere.workerFk = args.workerId;
timed: {between: [started, ended]}, tmpUser = new ParameterizedSQL(tmpUserSQL + ' WHERE id = ?', [args.workerId]);
isSendMail: true
}, myOptions);
const where = {
workerFk: args.workerId,
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: Date.vnNew(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`');
stmts.push(stmt);
stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE tmp.`user` SELECT id userFk FROM account.user WHERE id = ?', [args.workerId]);
stmts.push(stmt);
} else {
await models.WorkerTimeControl.destroyAll({
timed: {between: [started, ended]},
isSendMail: true
}, myOptions);
const where = {
year: args.year,
week: args.week
};
await models.WorkerTimeControlMail.updateAll(where, {
updated: Date.vnNew(), state: 'SENDED'
}, myOptions);
stmt = new ParameterizedSQL('DROP TEMPORARY TABLE IF EXISTS tmp.`user`');
stmts.push(stmt);
stmt = new ParameterizedSQL('CREATE TEMPORARY TABLE IF NOT EXISTS tmp.`user` SELECT id as userFk FROM vn.worker w JOIN account.`user` u ON u.id = w.id WHERE id IS NOT NULL');
stmts.push(stmt);
} }
await models.WorkerTimeControl.destroyAll(destroyAllWhere, myOptions);
await models.WorkerTimeControlMail.updateAll(updateAllWhere, {
updated: Date.vnNew(),
state: 'SENDED'
}, myOptions);
stmts.push(tmpUser);
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`CALL vn.timeControl_calculate(?, ?) `CALL vn.timeControl_calculate(?, ?)
`, [started, ended]); `, [started, ended]);

View File

@ -0,0 +1,103 @@
const models = require('vn-loopback/server/server').models;
describe('updateWorkerTimeControlMail()', () => {
it('should update WorkerTimeControlMail if exist record', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const args = {
workerId: 9,
week: 50,
year: 2000,
state: 'CONFIRMED'
};
const ctx = {args};
try {
const options = {transaction: tx};
const beforeMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
year: args.year,
week: args.week,
}
}, options);
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
const afterMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
year: args.year,
week: args.week,
}
}, options);
expect(beforeMail.state).toEqual('SENDED');
expect(afterMail.state).toEqual(args.state);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should insert WorkerTimeControlMail if exist record', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const args = {
workerId: 1,
week: 51,
year: 2000,
state: 'SENDED'
};
const ctx = {args};
try {
const options = {transaction: tx};
const beforeMail = await models.WorkerTimeControlMail.find({
where: {
workerFk: args.workerId,
year: args.year,
week: args.week,
}
}, options);
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
const afterMail = await models.WorkerTimeControlMail.find({
where: {
workerFk: args.workerId,
year: args.year,
week: args.week,
}
}, options);
expect(beforeMail).toEqual([]);
expect(afterMail.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw error if not exist any record in this week', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const ctx = {args: {
workerId: 1,
week: 1,
year: 0,
state: 'SENDED'
}};
let error;
try {
const options = {transaction: tx};
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`There aren't records for this week`);
});
});

View File

@ -40,30 +40,39 @@ module.exports = Self => {
Self.updateWorkerTimeControlMail = async(ctx, options) => { Self.updateWorkerTimeControlMail = async(ctx, options) => {
const models = Self.app.models; const models = Self.app.models;
const args = ctx.args; const args = ctx.args;
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({ const [sent] = await models.WorkerTimeControlMail.find({
where: { where: {
workerFk: args.workerId,
year: args.year, year: args.year,
week: args.week week: args.week,
} },
limit: 1
}, myOptions); }, myOptions);
if (!workerTimeControlMail) throw new UserError(`There aren't records for this week`); if (!sent) throw new UserError(`There aren't records for this week`);
await workerTimeControlMail.updateAttributes({ const workerTimeControlMail = await models.WorkerTimeControlMail.upsertWithWhere(
{
year: args.year,
week: args.week,
workerFk: args.workerId
},
{
state: args.state, state: args.state,
reason: args.reason || null reason: args.workerId,
}, myOptions); year: args.year,
week: args.week,
workerFk: args.workerId
},
myOptions);
if (args.state == 'SENDED') { if (args.state == 'SENDED') {
await workerTimeControlMail.updateAttributes({ await workerTimeControlMail.updateAttributes({
sendedCounter: workerTimeControlMail.sendedCounter + 1 sendedCounter: workerTimeControlMail.sendedCounter ? workerTimeControlMail.sendedCounter + 1 : 1
}, myOptions); }, myOptions);
} }
}; };

View File

@ -61,9 +61,8 @@ module.exports = Self => {
const url = `${salix.url}worker/${args.workerId}/time-control?timestamp=${timestamp}`; const url = `${salix.url}worker/${args.workerId}/time-control?timestamp=${timestamp}`;
ctx.args.url = url; ctx.args.url = url;
await Self.sendTemplate(ctx, 'weekly-hour-record'); await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions);
return Self.sendTemplate(ctx, 'weekly-hour-record');
return models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions);
}; };
function getMondayDateFromYearWeek(yearNumber, weekNumber) { function getMondayDateFromYearWeek(yearNumber, weekNumber) {

View File

@ -46,7 +46,7 @@ module.exports = Self => {
SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk
FROM worker w FROM worker w
JOIN account.user u ON u.id = w.id JOIN account.user u ON u.id = w.id
JOIN business b ON b.workerFk = w.id LEFT JOIN business b ON b.workerFk = w.id
) w`); ) w`);
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));

View File

@ -0,0 +1,48 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('setPassword', {
description: 'Set a new password',
accepts: [
{
arg: 'workerFk',
type: 'number',
required: true,
description: 'The worker id',
},
{
arg: 'newPass',
type: 'String',
required: true,
description: 'The new worker password'
}
],
http: {
path: `/:id/setPassword`,
verb: 'PATCH'
}
});
Self.setPassword = async(ctx, options) => {
const models = Self.app.models;
const myOptions = {};
const {args} = ctx;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const isSubordinate = await models.Worker.isSubordinate(ctx, args.workerFk, myOptions);
if (!isSubordinate) throw new UserError('You don\'t have enough privileges.');
await models.VnUser.setPassword(args.workerFk, args.newPass, myOptions);
await models.VnUser.updateAll({id: args.workerFk}, {emailVerified: true}, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,61 @@
const UserError = require('vn-loopback/util/user-error');
const models = require('vn-loopback/server/server').models;
describe('worker setPassword()', () => {
let ctx;
beforeAll(() => {
ctx = {
req: {
accessToken: {},
headers: {origin: 'http://localhost'}
},
args: {workerFk: 9}
};
});
beforeEach(() => {
ctx.req.accessToken.userId = 20;
ctx.args.newPass = 'H3rn4d3z#';
});
it('should change the password', async() => {
const tx = await models.Worker.beginTransaction({});
try {
const options = {transaction: tx};
await models.Worker.setPassword(ctx, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an error: Password does not meet requirements', async() => {
const tx = await models.Collection.beginTransaction({});
ctx.args.newPass = 'Hi';
try {
const options = {transaction: tx};
await models.Worker.setPassword(ctx, options);
await tx.rollback();
} catch (e) {
expect(e.sqlMessage).toEqual('Password does not meet requirements');
await tx.rollback();
}
});
it('should throw an error: You don\'t have enough privileges.', async() => {
ctx.req.accessToken.userId = 5;
const tx = await models.Collection.beginTransaction({});
try {
const options = {transaction: tx};
await models.Worker.setPassword(ctx, options);
await tx.rollback();
} catch (e) {
expect(e).toEqual(new UserError(`You don't have enough privileges.`));
await tx.rollback();
}
});
});

View File

@ -1,3 +0,0 @@
module.exports = Self => {
require('../methods/worker-mana/getCurrentWorkerMana')(Self);
};

View File

@ -18,6 +18,7 @@ module.exports = Self => {
require('../methods/worker/allocatePDA')(Self); require('../methods/worker/allocatePDA')(Self);
require('../methods/worker/search')(Self); require('../methods/worker/search')(Self);
require('../methods/worker/isAuthorized')(Self); require('../methods/worker/isAuthorized')(Self);
require('../methods/worker/setPassword')(Self);
Self.validatesUniquenessOf('locker', { Self.validatesUniquenessOf('locker', {
message: 'This locker has already been assigned' message: 'This locker has already been assigned'

View File

@ -47,6 +47,9 @@
}, },
"locker": { "locker": {
"type" : "number" "type" : "number"
},
"isF11Allowed": {
"type" : "boolean"
} }
}, },
"relations": { "relations": {

View File

@ -8,7 +8,7 @@ class Controller extends ModuleCard {
{ {
relation: 'user', relation: 'user',
scope: { scope: {
fields: ['name'], fields: ['name', 'emailVerified'],
include: { include: {
relation: 'emailUser', relation: 'emailUser',
scope: { scope: {

View File

@ -11,6 +11,9 @@
? 'Click to allow the user to be disabled' ? 'Click to allow the user to be disabled'
: 'Click to exclude the user from getting disabled'}} : 'Click to exclude the user from getting disabled'}}
</vn-item> </vn-item>
<vn-item ng-if="!$ctrl.worker.user.emailVerified" ng-click="setPassword.show()" translate>
Change password
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -28,11 +31,17 @@
</vn-label-value> </vn-label-value>
<vn-label-value <vn-label-value
label="Phone" label="Phone"
value="{{$ctrl.worker.phone}}"> >
<vn-link-phone
phone-number="$ctrl.worker.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value <vn-label-value
label="Extension" label="Extension"
value="{{$ctrl.worker.sip.extension}}"> >
<vn-link-phone
phone-number="$ctrl.worker.sip.extension"
></vn-link-phone>
</vn-label-value> </vn-label-value>
</div> </div>
<div class="icons"> <div class="icons">
@ -66,4 +75,29 @@
<vn-popup vn-id="summary"> <vn-popup vn-id="summary">
<vn-worker-summary worker="$ctrl.worker"></vn-worker-summary> <vn-worker-summary worker="$ctrl.worker"></vn-worker-summary>
</vn-popup> </vn-popup>
<vn-dialog
vn-id="setPassword"
on-accept="$ctrl.setPassword($ctrl.worker.password)"
message="Reset password"
>
<tpl-body>
<vn-textfield
vn-one
label="New password"
required="true"
ng-model="$ctrl.newPassword"
type="password"
info="{{'Password requirements' | translate:$ctrl.passRequirements}}"
>
</vn-textfield>
<vn-textfield
label="Repeat password"
ng-model="$ctrl.repeatPassword"
type="password">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,6 +1,6 @@
import ngModule from '../module'; import ngModule from '../module';
import Descriptor from 'salix/components/descriptor'; import Descriptor from 'salix/components/descriptor';
const UserError = require('vn-loopback/util/user-error');
class Controller extends Descriptor { class Controller extends Descriptor {
constructor($element, $, $rootScope) { constructor($element, $, $rootScope) {
super($element, $); super($element, $);
@ -13,9 +13,11 @@ class Controller extends Descriptor {
set worker(value) { set worker(value) {
this.entity = value; this.entity = value;
if (value) if (value)
this.getIsExcluded(); this.getIsExcluded();
if (this.entity && !this.entity.user.emailVerified)
this.getPassRequirements();
} }
getIsExcluded() { getIsExcluded() {
@ -39,7 +41,7 @@ class Controller extends Descriptor {
{ {
relation: 'user', relation: 'user',
scope: { scope: {
fields: ['name'], fields: ['name', 'emailVerified'],
include: { include: {
relation: 'emailUser', relation: 'emailUser',
scope: { scope: {
@ -67,10 +69,29 @@ class Controller extends Descriptor {
} }
] ]
}; };
return this.getData(`Workers/${this.id}`, {filter}) return this.getData(`Workers/${this.id}`, {filter})
.then(res => this.entity = res.data); .then(res => this.entity = res.data);
} }
getPassRequirements() {
this.$http.get('UserPasswords/findOne')
.then(res => {
this.passRequirements = res.data;
});
}
setPassword() {
if (!this.newPassword)
throw new UserError(`You must enter a new password`);
if (this.newPassword != this.repeatPassword)
throw new UserError(`Passwords don't match`);
this.$http.patch(
`Workers/${this.entity.id}/setPassword`,
{workerFk: this.entity.id, newPass: this.newPassword}
) .then(() => {
this.vnApp.showSuccess(this.$translate.instant('Password changed!'));
});
}
} }
Controller.$inject = ['$element', '$scope', '$rootScope']; Controller.$inject = ['$element', '$scope', '$rootScope'];

View File

@ -23,4 +23,24 @@ describe('vnWorkerDescriptor', () => {
expect(controller.worker).toEqual(response); expect(controller.worker).toEqual(response);
}); });
}); });
describe('setPassword()', () => {
it('should throw an error: You must enter a new password', () => {
try {
controller.setPassword();
} catch (error) {
expect(error.message).toEqual('You must enter a new password');
}
});
it('should throw an error: Passwords don\'t match', () => {
controller.newPassword = 'aaa';
controller.repeatPassword = 'bbb';
try {
controller.setPassword();
} catch (error) {
expect(error.message).toEqual('Passwords don\'t match');
}
});
});
}); });

View File

@ -42,14 +42,21 @@
{{::worker.boss.name}} {{::worker.boss.name}}
</span> </span>
</vn-label-value> </vn-label-value>
<vn-label-value label="Mobile extension" <vn-label-value label="Mobile extension">
value="{{worker.mobileExtension}}"> <vn-link-phone
phone-number="worker.mobileExtension"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Business phone" <vn-label-value label="Business phone">
value="{{worker.phone}}"> <vn-link-phone
phone-number="worker.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Personal phone" <vn-label-value label="Personal phone"
value="{{worker.client.phone}}"> >
<vn-link-phone
phone-number="worker.client.phone"
></vn-link-phone>
</vn-label-value> </vn-label-value>
<vn-label-value label="Locker" <vn-label-value label="Locker"
value="{{worker.locker}}"> value="{{worker.locker}}">

View File

@ -1,6 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import Summary from 'salix/components/summary'; import Summary from 'salix/components/summary';
class Controller extends Summary { class Controller extends Summary {
get worker() { get worker() {
return this._worker; return this._worker;

View File

@ -79,30 +79,32 @@
</vn-table> </vn-table>
</vn-card> </vn-card>
<vn-button-bar ng-show="$ctrl.state" class="vn-w-lg"> <vn-button-bar class="vn-w-lg">
<div ng-show="$ctrl.state">
<vn-button <vn-button
label="Satisfied" label="Satisfied"
disabled="$ctrl.state == 'CONFIRMED'" disabled="$ctrl.state == 'CONFIRMED'"
ng-if="$ctrl.isHimSelf" ng-show="$ctrl.isHimSelf"
ng-click="$ctrl.isSatisfied()"> ng-click="$ctrl.isSatisfied()">
</vn-button> </vn-button>
<vn-button <vn-button
label="Not satisfied" label="Not satisfied"
disabled="$ctrl.state == 'REVISE'" disabled="$ctrl.state == 'REVISE'"
ng-if="$ctrl.isHimSelf" ng-show="$ctrl.isHimSelf"
ng-click="reason.show()"> ng-click="reason.show()">
</vn-button> </vn-button>
<vn-button <vn-button
label="Reason" label="Reason"
ng-if="$ctrl.reason && ($ctrl.isHimSelf || $ctrl.isHr)" ng-show="$ctrl.reason && ($ctrl.isHimSelf || $ctrl.isHr)"
ng-click="reason.show()"> ng-click="reason.show()">
</vn-button> </vn-button>
</div>
<vn-button <vn-button
label="Resend" label="{{$ctrl.state ? 'Resend' : 'Send'}}"
ng-click="sendEmailConfirmation.show()" ng-click="sendEmailConfirmation.show()"
class="right" class="right"
vn-tooltip="Resend email of this week to the user" vn-tooltip="{{$ctrl.state ? 'Resend' : 'Send'}} email of this week to the user"
ng-show="::$ctrl.isHr"> ng-if="$ctrl.isHr && $ctrl.state != 'CONFIRMED' && $ctrl.canResend">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</div> </div>

View File

@ -53,6 +53,8 @@ class Controller extends Section {
set worker(value) { set worker(value) {
this._worker = value; this._worker = value;
this.fetchHours(); this.fetchHours();
if (this.date)
this.getWeekData();
} }
/** /**
@ -110,6 +112,7 @@ class Controller extends Section {
} }
if (!this.weekTotalHours) this.fetchHours(); if (!this.weekTotalHours) this.fetchHours();
if (this.worker)
this.getWeekData(); this.getWeekData();
} }
@ -127,17 +130,34 @@ class Controller extends Section {
workerFk: this.$params.id, workerFk: this.$params.id,
year: this._date.getFullYear(), year: this._date.getFullYear(),
week: this.getWeekNumber(this._date) week: this.getWeekNumber(this._date)
} },
}; };
this.$http.get('WorkerTimeControlMails', {filter}) this.$http.get('WorkerTimeControlMails', {filter})
.then(res => { .then(res => {
const mail = res.data; if (!res.data.length) {
if (!mail.length) {
this.state = null; this.state = null;
return; return;
} }
this.state = mail[0].state; const [mail] = res.data;
this.reason = mail[0].reason; this.state = mail.state;
this.reason = mail.reason;
});
this.canBeResend();
}
canBeResend() {
this.canResend = false;
const filter = {
where: {
year: this._date.getFullYear(),
week: this.getWeekNumber(this._date)
},
limit: 1
};
this.$http.get('WorkerTimeControlMails', {filter})
.then(res => {
if (res.data.length)
this.canResend = true;
}); });
} }
@ -356,30 +376,25 @@ class Controller extends Section {
} }
isSatisfied() { isSatisfied() {
const params = { this.updateWorkerTimeControlMail('CONFIRMED');
workerId: this.worker.id,
year: this.date.getFullYear(),
week: this.weekNumber,
state: 'CONFIRMED'
};
const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => {
this.getMailStates(this.date);
this.getWeekData();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
} }
isUnsatisfied() { isUnsatisfied() {
if (!this.reason) throw new UserError(`You must indicate a reason`); if (!this.reason) throw new UserError(`You must indicate a reason`);
this.updateWorkerTimeControlMail('REVISE', this.reason);
}
updateWorkerTimeControlMail(state, reason) {
const params = { const params = {
workerId: this.worker.id, workerId: this.worker.id,
year: this.date.getFullYear(), year: this.date.getFullYear(),
week: this.weekNumber, week: this.weekNumber,
state: 'REVISE', state
reason: this.reason
}; };
if (reason)
params.reason = reason;
const query = `WorkerTimeControls/updateWorkerTimeControlMail`; const query = `WorkerTimeControls/updateWorkerTimeControlMail`;
this.$http.post(query, params).then(() => { this.$http.post(query, params).then(() => {
this.getMailStates(this.date); this.getMailStates(this.date);

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