Merge branch 'dev' into 3126-changePackageFkTopackagingFk
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
f26595bb62
|
@ -18,6 +18,7 @@ describe('setSaleQuantity()', () => {
|
|||
|
||||
it('should change quantity sale', async() => {
|
||||
const tx = await models.Ticket.beginTransaction({});
|
||||
spyOn(models.Item, 'getVisibleAvailable').and.returnValue((new Promise(resolve => resolve({available: 100}))));
|
||||
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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});
|
||||
};
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||
|
||||
describe('loopback model VnUser', () => {
|
||||
it('should return true if the user has the given role', async() => {
|
||||
|
@ -12,4 +13,42 @@ describe('loopback model VnUser', () => {
|
|||
|
||||
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());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/url/getByUser')(Self);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
const vnModel = require('vn-loopback/common/models/vn-model');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
const {Email} = require('vn-print');
|
||||
const ForbiddenError = require('vn-loopback/util/forbiddenError');
|
||||
|
||||
module.exports = function(Self) {
|
||||
vnModel(Self);
|
||||
|
@ -12,6 +13,7 @@ module.exports = function(Self) {
|
|||
require('../methods/vn-user/privileges')(Self);
|
||||
require('../methods/vn-user/validate-auth')(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');
|
||||
|
||||
|
@ -178,45 +180,75 @@ module.exports = function(Self) {
|
|||
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
|
||||
.filter(acl => acl.property != 'changePassword');
|
||||
|
||||
// FIXME: https://redmine.verdnatura.es/issues/5761
|
||||
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => {
|
||||
// if (!ctx.args || !ctx.args.data.email) return;
|
||||
Self.userSecurity = async(ctx, userId, options) => {
|
||||
const models = Self.app.models;
|
||||
const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
|
||||
const ctxToken = {req: {accessToken}};
|
||||
|
||||
// const loopBackContext = LoopBackContext.getCurrentContext();
|
||||
// const httpCtx = {req: loopBackContext.active};
|
||||
// const httpRequest = httpCtx.req.http.req;
|
||||
// const headers = httpRequest.headers;
|
||||
// const origin = headers.origin;
|
||||
// const url = origin.split(':');
|
||||
if (userId === accessToken.userId) return;
|
||||
|
||||
// class Mailer {
|
||||
// async send(verifyOptions, cb) {
|
||||
// const params = {
|
||||
// url: verifyOptions.verifyHref,
|
||||
// recipient: verifyOptions.to,
|
||||
// lang: ctx.req.getLocale()
|
||||
// };
|
||||
const myOptions = {};
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
// const email = new Email('email-verify', params);
|
||||
// email.send();
|
||||
const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
|
||||
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 = {
|
||||
// type: 'email',
|
||||
// to: instance.email,
|
||||
// from: {},
|
||||
// redirect: `${origin}/#!/account/${instance.id}/basic-data?emailConfirmed`,
|
||||
// template: false,
|
||||
// mailer: new Mailer,
|
||||
// host: url[1].split('/')[2],
|
||||
// port: url[2],
|
||||
// protocol: url[0],
|
||||
// user: Self
|
||||
// };
|
||||
throw new ForbiddenError();
|
||||
};
|
||||
|
||||
// await instance.verify(options);
|
||||
// });
|
||||
Self.observe('after save', async ctx => {
|
||||
const instance = ctx?.instance;
|
||||
const newEmail = instance?.email;
|
||||
const oldEmail = ctx?.hookState?.oldInstance?.email;
|
||||
if (!ctx.isNewInstance && (!newEmail || !oldEmail || newEmail == oldEmail)) return;
|
||||
|
||||
const loopBackContext = LoopBackContext.getCurrentContext();
|
||||
const httpCtx = {req: loopBackContext.active};
|
||||
const httpRequest = httpCtx.req.http.req;
|
||||
const headers = httpRequest.headers;
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -13,15 +13,12 @@
|
|||
"type": "number",
|
||||
"id": true
|
||||
},
|
||||
"name": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"mysql": {
|
||||
"columnName": "name"
|
||||
}
|
||||
"type": "string"
|
||||
},
|
||||
"roleFk": {
|
||||
"type": "number",
|
||||
|
@ -38,6 +35,12 @@
|
|||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
},
|
||||
|
@ -137,7 +140,8 @@
|
|||
"image",
|
||||
"hasGrant",
|
||||
"realm",
|
||||
"email"
|
||||
"email",
|
||||
"emailVerified"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
|
||||
VALUES ('Worker','setPassword','*','ALLOW','ROLE','employee');
|
||||
|
||||
|
|
@ -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');
|
|
@ -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 ;
|
|
@ -2867,6 +2867,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
|
|||
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
|
||||
VALUES
|
||||
('lilium', 'development', 'http://localhost:9000/#/'),
|
||||
('hedera', 'development', 'http://localhost:9090/'),
|
||||
('salix', 'development', 'http://localhost:5000/#!/');
|
||||
|
||||
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
|
||||
|
|
|
@ -30434,6 +30434,7 @@ CREATE TABLE `item` (
|
|||
`editorFk` int(10) unsigned DEFAULT NULL,
|
||||
`recycledPlastic` 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`),
|
||||
UNIQUE KEY `item_supplyResponseFk_idx` (`supplyResponseFk`),
|
||||
KEY `Color` (`inkFk`),
|
||||
|
|
|
@ -189,5 +189,6 @@
|
|||
"The sales do not exists": "The sales do not exists",
|
||||
"Ticket without Route": "Ticket without route",
|
||||
"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"
|
||||
}
|
|
@ -320,5 +320,7 @@
|
|||
"The response is not a PDF": "La respuesta no es un PDF",
|
||||
"Ticket without Route": "Ticket sin ruta",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -21,5 +21,16 @@
|
|||
"model": "VnUser",
|
||||
"foreignKey": "account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW"
|
||||
}, {
|
||||
"accessType": "WRITE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<mg-ajax path="VnUsers/{{patch.params.id}}/update-user" options="vnPatch"></mg-ajax>
|
||||
<vn-watcher
|
||||
vn-id="watcher"
|
||||
url="VnUsers"
|
||||
data="$ctrl.user"
|
||||
id-value="$ctrl.$params.id"
|
||||
form="form">
|
||||
form="form"
|
||||
save="patch">
|
||||
</vn-watcher>
|
||||
<form
|
||||
name="form"
|
||||
|
@ -12,18 +12,18 @@
|
|||
<vn-card class="vn-pa-lg">
|
||||
<vn-vertical>
|
||||
<vn-textfield
|
||||
label="User"
|
||||
label="User"
|
||||
ng-model="$ctrl.user.name"
|
||||
rule="VnUser"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Nickname"
|
||||
label="Nickname"
|
||||
ng-model="$ctrl.user.nickname"
|
||||
rule="VnUser">
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Personal email"
|
||||
label="Personal email"
|
||||
ng-model="$ctrl.user.email"
|
||||
rule="VnUser">
|
||||
</vn-textfield>
|
||||
|
|
|
@ -18,5 +18,8 @@ ngModule.component('vnUserBasicData', {
|
|||
controller: Controller,
|
||||
require: {
|
||||
card: '^vnUserCard'
|
||||
},
|
||||
bindings: {
|
||||
user: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -78,7 +78,9 @@
|
|||
"state": "account.card.basicData",
|
||||
"component": "vn-user-basic-data",
|
||||
"description": "Basic data",
|
||||
"acl": ["itManagement"]
|
||||
"params": {
|
||||
"user": "$ctrl.user"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url" : "/log",
|
||||
|
|
|
@ -64,7 +64,7 @@ describe('claim regularizeClaim()', () => {
|
|||
|
||||
claimEnds = await importTicket(ticketId, claimId, userId, options);
|
||||
|
||||
for (claimEnd of claimEnds)
|
||||
for (const claimEnd of claimEnds)
|
||||
await claimEnd.updateAttributes({claimDestinationFk: trashDestination}, options);
|
||||
|
||||
let claimBefore = await models.Claim.findById(claimId, null, options);
|
||||
|
|
|
@ -17,6 +17,7 @@ Search claim by id or client name: Buscar reclamaciones por identificador o nomb
|
|||
Claim deleted!: Reclamación eliminada!
|
||||
claim: reclamación
|
||||
Photos: Fotos
|
||||
Development: Trazabilidad
|
||||
Go to the claim: Ir a la reclamación
|
||||
Sale tracking: Líneas preparadas
|
||||
Ticket tracking: Estados del ticket
|
||||
|
|
|
@ -131,6 +131,9 @@
|
|||
"nonRecycledPlastic": {
|
||||
"type": "number"
|
||||
},
|
||||
"minQuantity": {
|
||||
"type": "number"
|
||||
},
|
||||
"packingOut": {
|
||||
"type": "number"
|
||||
},
|
||||
|
@ -154,6 +157,10 @@
|
|||
"mysql":{
|
||||
"columnName": "doPhoto"
|
||||
}
|
||||
},
|
||||
"minQuantity": {
|
||||
"type": "number",
|
||||
"description": "Min quantity"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
rule
|
||||
info="Full name calculates based on tags 1-3. Is not recommended to change it manually">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
url="ItemTypes"
|
||||
label="Type"
|
||||
|
@ -50,6 +52,30 @@
|
|||
</div>
|
||||
</tpl-item>
|
||||
</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-autocomplete
|
||||
|
@ -128,30 +154,13 @@
|
|||
ng-model="$ctrl.item.stemMultiplier"
|
||||
vn-name="stemMultiplier">
|
||||
</vn-input-number>
|
||||
<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-input-number
|
||||
min="1"
|
||||
label="Minimum sales quantity"
|
||||
ng-model="$ctrl.item.minQuantity"
|
||||
vn-name="minQuantity"
|
||||
rule>
|
||||
</vn-input-number>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-input-number
|
||||
|
|
|
@ -16,3 +16,4 @@ This item does need a photo: Este artículo necesita una foto
|
|||
Do photo: Hacer foto
|
||||
Recycled Plastic: Plástico reciclado
|
||||
Non recycled plastic: Plástico no reciclado
|
||||
Minimum sales quantity: Cantidad mínima de venta
|
||||
|
|
|
@ -128,6 +128,9 @@
|
|||
<vn-label-value label="Non recycled plastic"
|
||||
value="{{$ctrl.summary.item.nonRecycledPlastic}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Minimum sales quantity"
|
||||
value="{{$ctrl.summary.item.minQuantity}}">
|
||||
</vn-label-value>
|
||||
</vn-one>
|
||||
<vn-one name="tags">
|
||||
<h4 ng-show="$ctrl.isBuyer || $ctrl.isReplenisher">
|
||||
|
|
|
@ -2,3 +2,4 @@ Barcode: Códigos de barras
|
|||
Other data: Otros datos
|
||||
Go to the item: Ir al artículo
|
||||
WarehouseFk: Calculado sobre el almacén de {{ warehouseName }}
|
||||
Minimum sales quantity: Cantidad mínima de venta
|
||||
|
|
|
@ -100,31 +100,32 @@ module.exports = Self => {
|
|||
));
|
||||
|
||||
stmt = new ParameterizedSQL(`
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.subName,
|
||||
i.image,
|
||||
i.tag5,
|
||||
i.value5,
|
||||
i.tag6,
|
||||
i.value6,
|
||||
i.tag7,
|
||||
i.value7,
|
||||
i.tag8,
|
||||
i.value8,
|
||||
i.stars,
|
||||
tci.price,
|
||||
tci.available,
|
||||
w.lastName AS lastName,
|
||||
w.firstName,
|
||||
tci.priceKg,
|
||||
ink.hex
|
||||
SELECT i.id,
|
||||
i.name,
|
||||
i.subName,
|
||||
i.image,
|
||||
i.tag5,
|
||||
i.value5,
|
||||
i.tag6,
|
||||
i.value6,
|
||||
i.tag7,
|
||||
i.value7,
|
||||
i.tag8,
|
||||
i.value8,
|
||||
i.stars,
|
||||
tci.price,
|
||||
tci.available,
|
||||
w.lastName,
|
||||
w.firstName,
|
||||
tci.priceKg,
|
||||
ink.hex,
|
||||
i.minQuantity
|
||||
FROM tmp.ticketCalculateItem tci
|
||||
JOIN vn.item i ON i.id = tci.itemFk
|
||||
JOIN vn.itemType it ON it.id = i.typeFk
|
||||
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
|
||||
if (orderBy.isTag) {
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
|
||||
</div>
|
||||
<img
|
||||
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
|
||||
ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
|
||||
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
|
||||
on-error-src/>
|
||||
</div>
|
||||
<div class="description">
|
||||
<h3 class="link"
|
||||
<h3 class="link"
|
||||
ng-click="itemDescriptor.show($event, item.id)">
|
||||
{{::item.name}}
|
||||
</h3>
|
||||
|
@ -37,13 +37,28 @@
|
|||
value="{{::item.value7}}">
|
||||
</vn-label-value>
|
||||
</div>
|
||||
<vn-rating ng-if="::item.stars"
|
||||
ng-model="::item.stars">
|
||||
</vn-rating>
|
||||
<vn-horizontal>
|
||||
<vn-one>
|
||||
<vn-rating ng-if="::item.stars"
|
||||
ng-model="::item.stars"/>
|
||||
</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="price">
|
||||
<vn-one>
|
||||
<span>{{::item.available}}</span>
|
||||
<span>{{::item.available}}</span>
|
||||
<span translate>to</span>
|
||||
<span>{{::item.price | currency:'EUR':2}}</span>
|
||||
</vn-one>
|
||||
|
@ -54,7 +69,7 @@
|
|||
</vn-icon-button>
|
||||
</div>
|
||||
<div class="priceKg" ng-show="::item.priceKg">
|
||||
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
|
||||
<span>Precio por kilo {{::item.priceKg | currency: 'EUR'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,4 +84,4 @@
|
|||
<vn-item-descriptor-popover
|
||||
vn-id="item-descriptor"
|
||||
warehouse-fk="$ctrl.vnConfig.warehouseFk">
|
||||
</vn-item-descriptor-popover>
|
||||
</vn-item-descriptor-popover>
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
Order created: Orden creada
|
||||
Minimal quantity: Cantidad mínima
|
|
@ -44,4 +44,7 @@ vn-order-catalog {
|
|||
height: 30px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
color: $color-alert;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ module.exports = Self => {
|
|||
|
||||
const refoundZoneId = refundAgencyMode.zones()[0].id;
|
||||
|
||||
if (salesIds) {
|
||||
if (salesIds.length) {
|
||||
const salesFilter = {
|
||||
where: {id: {inq: salesIds}},
|
||||
include: {
|
||||
|
@ -91,16 +91,14 @@ module.exports = Self => {
|
|||
await models.SaleComponent.create(components, myOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!refundTicket) {
|
||||
const servicesFilter = {
|
||||
where: {id: {inq: servicesIds}}
|
||||
};
|
||||
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 [firstTicketId] = ticketsIds;
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
|
||||
|
@ -114,8 +112,8 @@ module.exports = Self => {
|
|||
for (const service of services) {
|
||||
await models.TicketService.create({
|
||||
description: service.description,
|
||||
quantity: service.quantity,
|
||||
price: - service.price,
|
||||
quantity: - service.quantity,
|
||||
price: service.price,
|
||||
taxClassFk: service.taxClassFk,
|
||||
ticketFk: refundTicket.id,
|
||||
ticketServiceTypeFk: service.ticketServiceTypeFk,
|
||||
|
|
|
@ -1,21 +1,9 @@
|
|||
/* eslint max-len: ["error", { "code": 150 }]*/
|
||||
|
||||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
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 = {
|
||||
req: {
|
||||
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() => {
|
||||
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({});
|
||||
|
||||
let error;
|
||||
try {
|
||||
const options = {transaction: tx};
|
||||
|
||||
await models.Sale.updateQuantity(ctx, 17, 99, options);
|
||||
await models.Sale.updateQuantity(ctx, 17, 31, options);
|
||||
|
||||
await tx.rollback();
|
||||
} 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() => {
|
||||
const tx = await models.Sale.beginTransaction({});
|
||||
const saleId = 17;
|
||||
const buyerId = 35;
|
||||
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 {
|
||||
const options = {transaction: tx};
|
||||
|
@ -87,6 +92,8 @@ describe('sale updateQuantity()', () => {
|
|||
});
|
||||
|
||||
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 saleId = 25;
|
||||
const newQuantity = 4;
|
||||
|
@ -119,6 +126,8 @@ describe('sale updateQuantity()', () => {
|
|||
__: () => {}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
|
||||
|
||||
const saleId = 17;
|
||||
const newQuantity = -10;
|
||||
|
||||
|
@ -140,6 +149,8 @@ describe('sale updateQuantity()', () => {
|
|||
});
|
||||
|
||||
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 saleId = 13;
|
||||
const newQuantity = -10;
|
||||
|
@ -159,4 +170,70 @@ describe('sale updateQuantity()', () => {
|
|||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
let UserError = require('vn-loopback/util/user-error');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('updateQuantity', {
|
||||
|
@ -64,17 +63,6 @@ module.exports = Self => {
|
|||
|
||||
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 result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
|
||||
|
||||
|
|
|
@ -63,17 +63,6 @@ module.exports = Self => {
|
|||
}
|
||||
}, 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({
|
||||
ticketFk: id,
|
||||
itemFk: item.id,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
module.exports = Self => {
|
||||
require('../methods/sale/getClaimableFromTicket')(Self);
|
||||
require('../methods/sale/reserve')(Self);
|
||||
|
@ -13,4 +16,77 @@ module.exports = Self => {
|
|||
Self.validatesPresenceOf('concept', {
|
||||
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');
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -16,8 +16,8 @@ class Controller extends SearchPanel {
|
|||
this.$http.get('ItemPackingTypes', {filter}).then(res => {
|
||||
for (let ipt of res.data) {
|
||||
itemPackingTypes.push({
|
||||
code: ipt.code,
|
||||
description: this.$t(ipt.description)
|
||||
description: this.$t(ipt.description),
|
||||
code: ipt.code
|
||||
});
|
||||
}
|
||||
this.itemPackingTypes = itemPackingTypes;
|
||||
|
|
|
@ -163,26 +163,16 @@ export default class Controller extends Section {
|
|||
return {'futureId': value};
|
||||
case 'liters':
|
||||
return {'liters': value};
|
||||
case 'lines':
|
||||
return {'lines': value};
|
||||
case 'futureLiters':
|
||||
return {'futureLiters': value};
|
||||
case 'lines':
|
||||
return {'lines': value};
|
||||
case 'futureLines':
|
||||
return {'futureLines': value};
|
||||
case 'ipt':
|
||||
return {or:
|
||||
[
|
||||
{'ipt': {like: `%${value}%`}},
|
||||
{'ipt': null}
|
||||
]
|
||||
};
|
||||
return {'ipt': {like: `%${value}%`}};
|
||||
case 'futureIpt':
|
||||
return {or:
|
||||
[
|
||||
{'futureIpt': {like: `%${value}%`}},
|
||||
{'futureIpt': null}
|
||||
]
|
||||
};
|
||||
return {'futureIpt': {like: `%${value}%`}};
|
||||
case 'totalWithVat':
|
||||
return {'totalWithVat': value};
|
||||
case 'futureTotalWithVat':
|
||||
|
|
|
@ -18,3 +18,4 @@ Multiple invoice: Factura múltiple
|
|||
Make invoice...: Crear factura...
|
||||
Invoice selected tickets: Facturar tickets seleccionados
|
||||
Are you sure to invoice tickets: ¿Seguro que quieres facturar {{ticketsAmount}} tickets?
|
||||
Rounding: Redondeo
|
||||
|
|
|
@ -311,7 +311,7 @@
|
|||
clear-disabled="true"
|
||||
suffix="%">
|
||||
</vn-input-number>
|
||||
<vn-vertical ng-if="$ctrl.usesMana && $ctrl.currentWorkerMana != 0">
|
||||
<vn-vertical ng-if="$ctrl.usesMana">
|
||||
<vn-radio
|
||||
label="Promotion mana"
|
||||
val="mana"
|
||||
|
|
|
@ -97,14 +97,6 @@ class Controller extends Section {
|
|||
});
|
||||
});
|
||||
this.getUsesMana();
|
||||
this.getCurrentWorkerMana();
|
||||
}
|
||||
|
||||
getCurrentWorkerMana() {
|
||||
this.$http.get(`WorkerManas/getCurrentWorkerMana`)
|
||||
.then(res => {
|
||||
this.currentWorkerMana = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
getUsesMana() {
|
||||
|
|
|
@ -120,12 +120,10 @@ describe('Ticket', () => {
|
|||
const expectedAmount = 250;
|
||||
$httpBackend.expect('GET', 'Tickets/1/getSalesPersonMana').respond(200, expectedAmount);
|
||||
$httpBackend.expect('GET', 'Sales/usesMana').respond(200);
|
||||
$httpBackend.expect('GET', 'WorkerManas/getCurrentWorkerMana').respond(200, expectedAmount);
|
||||
controller.getMana();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.edit.mana).toEqual(expectedAmount);
|
||||
expect(controller.currentWorkerMana).toEqual(expectedAmount);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
disabled="watcher.dataChanged() || !$ctrl.checkeds.length"
|
||||
label="Pay"
|
||||
ng-click="$ctrl.createRefund()"
|
||||
vn-acl="invoicing, claimManager, salesAssistant"
|
||||
vn-acl="invoicing, claimManager, salesAssistant, buyer"
|
||||
vn-acl-action="remove">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
|
@ -37,7 +37,9 @@
|
|||
<vn-check
|
||||
tabindex="1"
|
||||
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-autocomplete vn-two vn-focus
|
||||
data="ticketServiceTypes"
|
||||
|
@ -68,7 +70,8 @@
|
|||
vn-one
|
||||
label="Price"
|
||||
ng-model="service.price"
|
||||
step="0.01">
|
||||
step="0.01"
|
||||
min="0">
|
||||
</vn-input-number>
|
||||
<vn-auto>
|
||||
<vn-icon-button
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -66,46 +66,36 @@ module.exports = Self => {
|
|||
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.timeControlCalculate');
|
||||
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) {
|
||||
await models.WorkerTimeControl.destroyAll({
|
||||
userFk: args.workerId,
|
||||
timed: {between: [started, ended]},
|
||||
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);
|
||||
destroyAllWhere.userFk = args.workerId;
|
||||
updateAllWhere.workerFk = args.workerId;
|
||||
tmpUser = new ParameterizedSQL(tmpUserSQL + ' WHERE id = ?', [args.workerId]);
|
||||
}
|
||||
|
||||
await models.WorkerTimeControl.destroyAll(destroyAllWhere, myOptions);
|
||||
|
||||
await models.WorkerTimeControlMail.updateAll(updateAllWhere, {
|
||||
updated: Date.vnNew(),
|
||||
state: 'SENDED'
|
||||
}, myOptions);
|
||||
|
||||
stmts.push(tmpUser);
|
||||
|
||||
stmt = new ParameterizedSQL(
|
||||
`CALL vn.timeControl_calculate(?, ?)
|
||||
`, [started, ended]);
|
||||
|
|
|
@ -46,7 +46,7 @@ module.exports = Self => {
|
|||
SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk
|
||||
FROM worker w
|
||||
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`);
|
||||
|
||||
stmt.merge(conn.makeSuffix(filter));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/worker-mana/getCurrentWorkerMana')(Self);
|
||||
};
|
|
@ -18,6 +18,7 @@ module.exports = Self => {
|
|||
require('../methods/worker/allocatePDA')(Self);
|
||||
require('../methods/worker/search')(Self);
|
||||
require('../methods/worker/isAuthorized')(Self);
|
||||
require('../methods/worker/setPassword')(Self);
|
||||
|
||||
Self.validatesUniquenessOf('locker', {
|
||||
message: 'This locker has already been assigned'
|
||||
|
|
|
@ -8,7 +8,7 @@ class Controller extends ModuleCard {
|
|||
{
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
fields: ['name', 'emailVerified'],
|
||||
include: {
|
||||
relation: 'emailUser',
|
||||
scope: {
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
? 'Click to allow the user to be disabled'
|
||||
: 'Click to exclude the user from getting disabled'}}
|
||||
</vn-item>
|
||||
<vn-item ng-if="!$ctrl.worker.user.emailVerified" ng-click="setPassword.show()" translate>
|
||||
Change password
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
<slot-body>
|
||||
<div class="attributes">
|
||||
|
@ -72,4 +75,29 @@
|
|||
<vn-popup vn-id="summary">
|
||||
<vn-worker-summary worker="$ctrl.worker"></vn-worker-summary>
|
||||
</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>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import ngModule from '../module';
|
||||
import Descriptor from 'salix/components/descriptor';
|
||||
const UserError = require('vn-loopback/util/user-error');
|
||||
class Controller extends Descriptor {
|
||||
constructor($element, $, $rootScope) {
|
||||
super($element, $);
|
||||
|
@ -12,9 +13,11 @@ class Controller extends Descriptor {
|
|||
|
||||
set worker(value) {
|
||||
this.entity = value;
|
||||
|
||||
if (value)
|
||||
this.getIsExcluded();
|
||||
|
||||
if (this.entity && !this.entity.user.emailVerified)
|
||||
this.getPassRequirements();
|
||||
}
|
||||
|
||||
getIsExcluded() {
|
||||
|
@ -38,7 +41,7 @@ class Controller extends Descriptor {
|
|||
{
|
||||
relation: 'user',
|
||||
scope: {
|
||||
fields: ['name'],
|
||||
fields: ['name', 'emailVerified'],
|
||||
include: {
|
||||
relation: 'emailUser',
|
||||
scope: {
|
||||
|
@ -66,10 +69,29 @@ class Controller extends Descriptor {
|
|||
}
|
||||
]
|
||||
};
|
||||
|
||||
return this.getData(`Workers/${this.id}`, {filter})
|
||||
.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'];
|
||||
|
|
|
@ -23,4 +23,24 @@ describe('vnWorkerDescriptor', () => {
|
|||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,18 +30,21 @@ module.exports = Self => {
|
|||
Self.toggleIsIncluded = async(id, geoId, isIncluded, options) => {
|
||||
const models = Self.app.models;
|
||||
const myOptions = {};
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
if (isIncluded === undefined)
|
||||
return models.ZoneIncluded.destroyAll({zoneFk: id, geoFk: geoId}, myOptions);
|
||||
else {
|
||||
return models.ZoneIncluded.upsert({
|
||||
zoneFk: id,
|
||||
geoFk: geoId,
|
||||
isIncluded: isIncluded
|
||||
}, myOptions);
|
||||
}
|
||||
|
||||
const zoneIncluded = await models.ZoneIncluded.findOne({where: {zoneFk: id, geoFk: geoId}}, myOptions);
|
||||
|
||||
if (zoneIncluded)
|
||||
return zoneIncluded.updateAttribute('isIncluded', isIncluded, myOptions);
|
||||
|
||||
return models.ZoneIncluded.create({
|
||||
zoneFk: id,
|
||||
geoFk: geoId,
|
||||
isIncluded: isIncluded
|
||||
}, myOptions);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
}
|
||||
},
|
||||
"properties": {
|
||||
"zoneFk": {
|
||||
"id": true,
|
||||
"id": {
|
||||
"id": true,
|
||||
"type": "number"
|
||||
},
|
||||
"isIncluded": {
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
|
||||
<td class="centered" width="5%"></td>
|
||||
<td class="centered">{{service.taxDescription}}</td>
|
||||
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
|
||||
<td class="number">{{service.total | currency('EUR', $i18n.locale)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
SELECT
|
||||
tc.code taxDescription,
|
||||
ts.description,
|
||||
ts.quantity,
|
||||
ts.price
|
||||
ts.quantity,
|
||||
ts.price,
|
||||
ts.quantity * ts.price total
|
||||
FROM ticketService ts
|
||||
JOIN taxClass tc ON tc.id = ts.taxClassFk
|
||||
WHERE ts.ticketFk = ?
|
Loading…
Reference in New Issue