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

This commit is contained in:
Pablo Natek 2023-10-20 11:36:24 +02:00
commit 25c13e42ab
93 changed files with 2635 additions and 458 deletions

View File

@ -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};

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 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());
}
});
});
});

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 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);
});
};

View File

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

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,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,5 @@
ALTER TABLE `vn`.`buy` CHANGE `packageFk` `packagingFk` varchar(10)
CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '--' NULL;
ALTER TABLE `vn`.`buy`
ADD COLUMN `packageFk` varchar(10) AS (`packagingFk`) VIRTUAL;

View File

@ -0,0 +1,146 @@
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn2008`.`Compres`
AS SELECT `c`.`id` AS `Id_Compra`,
`c`.`entryFk` AS `Id_Entrada`,
`c`.`itemFk` AS `Id_Article`,
`c`.`buyingValue` AS `Costefijo`,
`c`.`quantity` AS `Cantidad`,
`c`.`packagingFk` AS `Id_Cubo`,
`c`.`stickers` AS `Etiquetas`,
`c`.`freightValue` AS `Portefijo`,
`c`.`packageValue` AS `Embalajefijo`,
`c`.`comissionValue` AS `Comisionfija`,
`c`.`packing` AS `Packing`,
`c`.`grouping` AS `grouping`,
`c`.`groupingMode` AS `caja`,
`c`.`location` AS `Nicho`,
`c`.`price1` AS `Tarifa1`,
`c`.`price2` AS `Tarifa2`,
`c`.`price3` AS `Tarifa3`,
`c`.`minPrice` AS `PVP`,
`c`.`printedStickers` AS `Vida`,
`c`.`isChecked` AS `punteo`,
`c`.`ektFk` AS `buy_edi_id`,
`c`.`created` AS `odbc_date`,
`c`.`isIgnored` AS `Novincular`,
`c`.`isPickedOff` AS `isPickedOff`,
`c`.`workerFk` AS `Id_Trabajador`,
`c`.`weight` AS `weight`,
`c`.`dispatched` AS `dispatched`,
`c`.`containerFk` AS `container_id`,
`c`.`itemOriginalFk` AS `itemOriginalFk`
FROM `vn`.`buy` `c`;
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn2008`.`buySource`
AS SELECT `b`.`entryFk` AS `Id_Entrada`,
`b`.`isPickedOff` AS `isPickedOff`,
NULL AS `tarifa0`,
`e`.`kop` AS `kop`,
`b`.`id` AS `Id_Compra`,
`i`.`typeFk` AS `tipo_id`,
`b`.`itemFk` AS `Id_Article`,
`i`.`size` AS `Medida`,
`i`.`stems` AS `Tallos`,
`b`.`stickers` AS `Etiquetas`,
`b`.`packagingFk` AS `Id_Cubo`,
`b`.`buyingValue` AS `Costefijo`,
`b`.`packing` AS `Packing`,
`b`.`grouping` AS `Grouping`,
`b`.`quantity` AS `Cantidad`,
`b`.`price2` AS `Tarifa2`,
`b`.`price3` AS `Tarifa3`,
`b`.`isChecked` AS `Punteo`,
`b`.`groupingMode` AS `Caja`,
`i`.`isToPrint` AS `Imprimir`,
`i`.`name` AS `Article`,
`vn`.`ink`.`picture` AS `Tinta`,
`i`.`originFk` AS `id_origen`,
`i`.`minPrice` AS `PVP`,
NULL AS `Id_Accion`,
`s`.`company_name` AS `pro`,
`i`.`hasMinPrice` AS `Min`,
`b`.`isIgnored` AS `Novincular`,
`b`.`freightValue` AS `Portefijo`,
round(`b`.`buyingValue` * `b`.`quantity`, 2) AS `Importe`,
`b`.`printedStickers` AS `Vida`,
`i`.`comment` AS `reference`,
`b`.`workerFk` AS `Id_Trabajador`,
`e`.`s1` AS `S1`,
`e`.`s2` AS `S2`,
`e`.`s3` AS `S3`,
`e`.`s4` AS `S4`,
`e`.`s5` AS `S5`,
`e`.`s6` AS `S6`,
0 AS `price_fixed`,
`i`.`producerFk` AS `producer_id`,
`i`.`subName` AS `tag1`,
`i`.`value5` AS `tag2`,
`i`.`value6` AS `tag3`,
`i`.`value7` AS `tag4`,
`i`.`value8` AS `tag5`,
`i`.`value9` AS `tag6`,
`s`.`company_name` AS `company_name`,
`b`.`weight` AS `weightPacking`,
`i`.`packingOut` AS `packingOut`,
`b`.`itemOriginalFk` AS `itemOriginalFk`,
`io`.`longName` AS `itemOriginalName`,
`it`.`gramsMax` AS `gramsMax`
FROM (
(
(
(
(
(
`vn`.`item` `i`
JOIN `vn`.`itemType` `it` ON(`it`.`id` = `i`.`typeFk`)
)
LEFT JOIN `vn`.`ink` ON(`vn`.`ink`.`id` = `i`.`inkFk`)
)
LEFT JOIN `vn`.`buy` `b` ON(`b`.`itemFk` = `i`.`id`)
)
LEFT JOIN `vn`.`item` `io` ON(`io`.`id` = `b`.`itemOriginalFk`)
)
LEFT JOIN `edi`.`ekt` `e` ON(`e`.`id` = `b`.`ektFk`)
)
LEFT JOIN `edi`.`supplier` `s` ON(`e`.`pro` = `s`.`supplier_id`)
);
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn`.`awbVolume`
AS SELECT `d`.`awbFk` AS `awbFk`,
`b`.`stickers` * `i`.`density` * IF(
`p`.`volume` > 0,
`p`.`volume`,
`p`.`width` * `p`.`depth` * IF(`p`.`height` = 0, `i`.`size` + 10, `p`.`height`)
) / (`vc`.`aerealVolumetricDensity` * 1000) AS `volume`,
`b`.`id` AS `buyFk`
FROM (
(
(
(
(
(
(
(
`vn`.`buy` `b`
JOIN `vn`.`item` `i` ON(`b`.`itemFk` = `i`.`id`)
)
JOIN `vn`.`itemType` `it` ON(`i`.`typeFk` = `it`.`id`)
)
JOIN `vn`.`packaging` `p` ON(`p`.`id` = `b`.`packagingFk`)
)
JOIN `vn`.`entry` `e` ON(`b`.`entryFk` = `e`.`id`)
)
JOIN `vn`.`travel` `t` ON(`t`.`id` = `e`.`travelFk`)
)
JOIN `vn`.`duaEntry` `de` ON(`de`.`entryFk` = `e`.`id`)
)
JOIN `vn`.`dua` `d` ON(`d`.`id` = `de`.`duaFk`)
)
JOIN `vn`.`volumeConfig` `vc`
)
WHERE `t`.`shipped` > makedate(year(`util`.`VN_CURDATE`()) - 1, 1);

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

@ -0,0 +1,57 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`buy_afterUpdate`
AFTER UPDATE ON `buy`
FOR EACH ROW
trig: BEGIN
DECLARE vLanded DATE;
DECLARE vBuyerFk INT;
DECLARE vIsBuyerToBeEmailed BOOL;
DECLARE vItemName VARCHAR(50);
IF @isModeInventory OR @isTriggerDisabled THEN
LEAVE trig;
END IF;
IF !(NEW.id <=> OLD.id)
OR !(NEW.entryFk <=> OLD.entryFk)
OR !(NEW.itemFk <=> OLD.itemFk)
OR !(NEW.quantity <=> OLD.quantity)
OR !(NEW.created <=> OLD.created) THEN
CALL stock.log_add('buy', NEW.id, OLD.id);
END IF;
CALL buy_afterUpsert(NEW.id);
SELECT w.isBuyerToBeEmailed, t.landed
INTO vIsBuyerToBeEmailed, vLanded
FROM entry e
JOIN travel t ON t.id = e.travelFk
JOIN warehouse w ON w.id = t.warehouseInFk
WHERE e.id = NEW.entryFk;
SELECT it.workerFk, i.longName
INTO vBuyerFk, vItemName
FROM itemCategory k
JOIN itemType it ON it.categoryFk = k.id
JOIN item i ON i.typeFk = it.id
WHERE i.id = OLD.itemFk;
IF vIsBuyerToBeEmailed AND
vBuyerFk != account.myUser_getId() AND
vLanded = util.VN_CURDATE() THEN
IF !(NEW.itemFk <=> OLD.itemFk) OR
!(NEW.quantity <=> OLD.quantity) OR
!(NEW.packing <=> OLD.packing) OR
!(NEW.grouping <=> OLD.grouping) OR
!(NEW.packagingFk <=> OLD.packagingFk) OR
!(NEW.weight <=> OLD.weight) THEN
CALL vn.mail_insert(
CONCAT(account.user_getNameFromId(vBuyerFk),'@verdnatura.es'),
CONCAT(account.myUser_getName(),'@verdnatura.es'),
CONCAT('E ', NEW.entryFk ,' Se ha modificado item ', NEW.itemFk, ' ', vItemName),
'Este email se ha generado automáticamente'
);
END IF;
END IF;
END$$
DELIMITER ;

File diff suppressed because it is too large Load Diff

View File

@ -1454,7 +1454,7 @@ INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `itemFk`, `itemTypeF
('HankPym', YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', 6, 1, '186', '0', '0.0'),
('HankPym', YEAR(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(util.VN_CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', 7, 1, '277', '0', '0.0');
INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`, `printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`)
INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packagingFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`, `printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`)
VALUES
(1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH)),
(2, 2, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0, 1, 0, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH)),
@ -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`)

View File

@ -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`),

View File

@ -1192,7 +1192,7 @@ export default {
secondBuyPacking: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.packing"]',
secondBuyWeight: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.weight"]',
secondBuyStickers: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.stickers"]',
secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packageFk"]',
secondBuyPackage: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.packagingFk"]',
secondBuyQuantity: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-input-number[ng-model="buy.quantity"]',
secondBuyItem: 'vn-entry-buy-index tbody:nth-child(3) > tr:nth-child(1) vn-autocomplete[ng-model="buy.itemFk"]',
importButton: 'vn-entry-buy-index vn-icon[icon="publish"]',

View File

@ -1,20 +1,5 @@
import getBrowser from '../../helpers/puppeteer';
const $ = {
saveButton: 'vn-supplier-fiscal-data button[type="submit"]',
};
const $inputs = {
province: 'vn-supplier-fiscal-data [name="province"]',
country: 'vn-supplier-fiscal-data [name="country"]',
postcode: 'vn-supplier-fiscal-data [name="postcode"]',
city: 'vn-supplier-fiscal-data [name="city"]',
socialName: 'vn-supplier-fiscal-data [name="socialName"]',
taxNumber: 'vn-supplier-fiscal-data [name="taxNumber"]',
account: 'vn-supplier-fiscal-data [name="account"]',
sageWithholding: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageWithholdingFk"]',
sageTaxType: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageTaxTypeFk"]'
};
describe('Supplier fiscal data path', () => {
let browser;
let page;
@ -30,7 +15,7 @@ describe('Supplier fiscal data path', () => {
await browser.close();
});
it('should attempt to edit the fiscal data and check data is saved', async() => {
it('should attempt to edit the fiscal data and check data iss saved', async() => {
await page.accessToSection('supplier.card.fiscalData');
const form = 'vn-supplier-fiscal-data form';
@ -40,16 +25,16 @@ describe('Supplier fiscal data path', () => {
postcode: null,
city: 'Valencia',
socialName: 'Farmer King SL',
taxNumber: 'Wrong tax number',
taxNumber: '12345678Z',
account: '0123456789',
sageWithholding: 'retencion estimacion objetiva',
sageTaxType: 'operaciones no sujetas'
};
const errorMessage = await page.sendForm(form, values);
const message = await page.sendForm(form, {
taxNumber: '12345678Z'
const errorMessage = await page.sendForm(form, {
taxNumber: 'Wrong tax number'
});
const message = await page.sendForm(form, values);
await page.reloadSection('supplier.card.fiscalData');
const formValues = await page.fetchForm(form, Object.keys(values));

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}]
}

View File

@ -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>

View File

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

View File

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

View File

@ -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);

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: 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

View File

@ -1,15 +1,24 @@
const models = require('vn-loopback/server/server').models;
describe('Client transactions', () => {
const ctx = {};
it('should call transactions() method to receive a list of Web Payments from BRUCE WAYNE', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {};
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();
@ -26,9 +35,17 @@ describe('Client transactions', () => {
try {
const options = {transaction: tx};
const ctx = {args: {orderFk: 6}};
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];
@ -47,10 +64,17 @@ describe('Client transactions', () => {
try {
const options = {transaction: tx};
const ctx = {args: {amount: 40}};
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 transaction = result[randomIndex];
@ -63,4 +87,43 @@ describe('Client transactions', () => {
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',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
},
{
arg: 'orderFk',
type: 'number',
http: {source: 'query'}
},
{
arg: 'clientFk',
type: 'number',
http: {source: 'query'}
},
{
arg: 'amount',
type: 'number',
http: {source: 'query'}
},
{
arg: 'from',
type: 'date',
},
{
arg: 'to',
type: 'date',
}
],
returns: {
@ -40,14 +44,15 @@ module.exports = Self => {
}
});
Self.transactions = async(ctx, filter, options) => {
const args = ctx.args;
Self.transactions = async(ctx, filter, orderFk, clientFk, amount, from, to, options) => {
const myOptions = {};
if (typeof options == 'object')
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) {
case 'orderFk':
return {'t.id': value};
@ -55,6 +60,10 @@ module.exports = Self => {
return {'t.clientFk': value};
case 'amount':
return {'t.amount': (value * 100)};
case 'from':
return {'t.created': {gte: value}};
case 'to':
return {'t.created': {lte: value}};
}
});

View File

@ -15,4 +15,4 @@ columns:
weight: weight
entryFk: entry
itemFk: item
packageFk: package
packagingFk: package

View File

@ -15,4 +15,4 @@ columns:
weight: peso
entryFk: entrada
itemFk: artículo
packageFk: paquete
packagingFk: paquete

View File

@ -80,7 +80,7 @@ module.exports = Self => {
comissionValue: buyUltimate.comissionValue,
packageValue: buyUltimate.packageValue,
location: buyUltimate.location,
packageFk: buyUltimate.packageFk,
packagingFk: buyUltimate.packagingFk,
price1: buyUltimate.price1,
price2: buyUltimate.price2,
price3: buyUltimate.price3,

View File

@ -44,7 +44,7 @@ module.exports = Self => {
'grouping',
'groupingMode',
'quantity',
'packageFk',
'packagingFk',
'weight',
'buyingValue',
'price2',

View File

@ -108,7 +108,7 @@ module.exports = Self => {
packing: buy.packing,
grouping: buy.grouping,
buyingValue: buy.buyingValue,
packageFk: buy.packageFk,
packagingFk: buy.packagingFk,
groupingMode: lastBuy.groupingMode,
weight: lastBuy.weight
});

View File

@ -39,7 +39,7 @@ module.exports = Self => {
}, myOptions);
if (packaging)
buy.packageFk = packaging.id;
buy.packagingFk = packaging.id;
const reference = await models.ItemMatchProperties.findOne({
fields: ['itemFk'],

View File

@ -153,64 +153,63 @@ module.exports = Self => {
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
stmt = new ParameterizedSQL(`
SELECT
i.image,
i.id AS itemFk,
i.size,
i.weightByPiece,
it.code,
i.typeFk,
i.family,
i.isActive,
i.minPrice,
i.description,
i.name,
i.subName,
i.packingOut,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10,
t.name AS type,
intr.description AS intrastat,
ori.code AS origin,
b.entryFk,
b.id,
b.quantity,
b.buyingValue,
b.freightValue,
b.isIgnored,
b.packing,
b.grouping,
b.groupingMode,
b.comissionValue,
b.packageValue,
b.price2,
b.price3,
b.ektFk,
b.weight,
b.packageFk,
lb.landing
FROM cache.last_buy lb
LEFT JOIN cache.visible v ON v.item_id = lb.item_id
AND v.calc_id = @calc_id
JOIN item i ON i.id = lb.item_id
JOIN itemType it ON it.id = i.typeFk AND lb.warehouse_id = ?
JOIN buy b ON b.id = lb.buy_id
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN entry e ON e.id = b.entryFk AND e.created >= DATE_SUB(? ,INTERVAL 1 YEAR)
LEFT JOIN supplier s ON s.id = e.supplierFk`
SELECT i.image,
i.id AS itemFk,
i.size,
i.weightByPiece,
it.code,
i.typeFk,
i.family,
i.isActive,
i.minPrice,
i.description,
i.name,
i.subName,
i.packingOut,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
i.tag8,
i.value8,
i.tag9,
i.value9,
i.tag10,
i.value10,
t.name AS type,
intr.description AS intrastat,
ori.code AS origin,
b.entryFk,
b.id,
b.quantity,
b.buyingValue,
b.freightValue,
b.isIgnored,
b.packing,
b.grouping,
b.groupingMode,
b.comissionValue,
b.packageValue,
b.price2,
b.price3,
b.ektFk,
b.weight,
b.packagingFk,
lb.landing
FROM cache.last_buy lb
LEFT JOIN cache.visible v ON v.item_id = lb.item_id
AND v.calc_id = @calc_id
JOIN item i ON i.id = lb.item_id
JOIN itemType it ON it.id = i.typeFk AND lb.warehouse_id = ?
JOIN buy b ON b.id = lb.buy_id
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN entry e ON e.id = b.entryFk AND e.created >= DATE_SUB(? ,INTERVAL 1 YEAR)
LEFT JOIN supplier s ON s.id = e.supplierFk`
, [userConfig.warehouseFk, date]);
if (ctx.args.tags) {

View File

@ -33,7 +33,7 @@ describe('entry import()', () => {
packing: 1,
size: 1,
volume: 1200,
packageFk: '94'
packagingFk: '94'
},
{
itemFk: 4,
@ -43,7 +43,7 @@ describe('entry import()', () => {
packing: 1,
size: 25,
volume: 1125,
packageFk: '94'
packagingFk: '94'
}
]
}

View File

@ -10,12 +10,12 @@ describe('entry importBuysPreview()', () => {
});
});
it('should return the buys with the calculated packageFk', async() => {
it('should return the buys with the calculated packagingFk', async() => {
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const expectedPackageFk = '3';
const expectedPackagingFk = '3';
const buys = [
{
itemFk: 1,
@ -39,7 +39,7 @@ describe('entry importBuysPreview()', () => {
const randomIndex = Math.floor(Math.random() * result.length);
const buy = result[randomIndex];
expect(buy.packageFk).toEqual(expectedPackageFk);
expect(buy.packagingFk).toEqual(expectedPackagingFk);
await tx.rollback();
} catch (e) {

View File

@ -103,7 +103,7 @@
"package": {
"type": "belongsTo",
"model": "Packaging",
"foreignKey": "packageFk"
"foreignKey": "packagingFk"
},
"worker": {
"type": "belongsTo",

View File

@ -83,14 +83,14 @@
<td center>{{::buy.packing | dashIfEmpty}}</td>
<td center>{{::buy.grouping | dashIfEmpty}}</td>
<td>{{::buy.buyingValue | currency: 'EUR':2}}</td>
<td center title="{{::buy.packageFk | dashIfEmpty}}">
<td center title="{{::buy.packagingFk | dashIfEmpty}}">
<vn-autocomplete
vn-one
url="Packagings"
show-field="id"
value-field="id"
where="{isBox: true}"
ng-model="buy.packageFk">
ng-model="buy.packagingFk">
</vn-autocomplete>
</td>
</tr>

View File

@ -88,12 +88,12 @@
<td center>
<vn-autocomplete
vn-one
title="{{::buy.packageFk | dashIfEmpty}}"
title="{{::buy.packagingFk | dashIfEmpty}}"
url="Packagings"
show-field="id"
value-field="id"
where="{freightItemFk: true}"
ng-model="buy.packageFk"
ng-model="buy.packagingFk"
on-change="$ctrl.saveBuy(buy)">
</vn-autocomplete>
</td>

View File

@ -4,7 +4,7 @@ import Section from 'salix/components/section';
export default class Controller extends Section {
saveBuy(buy) {
const missingData = !buy.itemFk || !buy.quantity || !buy.packageFk;
const missingData = !buy.itemFk || !buy.quantity || !buy.packagingFk;
if (missingData) return;
let options;

View File

@ -17,7 +17,7 @@ describe('Entry buy', () => {
describe('saveBuy()', () => {
it(`should call the buys patch route if the received buy has an ID`, () => {
const buy = {id: 1, itemFk: 1, quantity: 1, packageFk: 1};
const buy = {id: 1, itemFk: 1, quantity: 1, packagingFk: 1};
const query = `Buys/${buy.id}`;

View File

@ -104,7 +104,7 @@
<th field="weight">
<span translate>Weight</span>
</th>
<th field="packageFk">
<th field="packagingFk">
<span translate>Package</span>
</th>
<th field="packingOut">
@ -207,7 +207,7 @@
<td number>{{::buy.minPrice | currency: 'EUR':3}}</td>
<td>{{::buy.ektFk | dashIfEmpty}}</td>
<td>{{::buy.weight}}</td>
<td>{{::buy.packageFk}}</td>
<td>{{::buy.packagingFk}}</td>
<td>{{::buy.packingOut}}</td>
<td>{{::buy.landing | date: 'dd/MM/yyyy'}}</td>
</tr>

View File

@ -48,7 +48,7 @@ export default class Controller extends Section {
}
},
{
field: 'packageFk',
field: 'packagingFk',
autocomplete: {
url: 'Packagings',
showField: 'id'
@ -133,7 +133,7 @@ export default class Controller extends Section {
case 'price3':
case 'ektFk':
case 'weight':
case 'packageFk':
case 'packagingFk':
return {[`b.${param}`]: value};
}
}

View File

@ -105,7 +105,7 @@
<tr>
<th translate center field="quantity">Quantity</th>
<th translate center field="sticker">Stickers</th>
<th translate center field="packageFk">Package</th>
<th translate center field="packagingFk">Package</th>
<th translate center field="weight">Weight</th>
<th translate center field="packing">Packing</th>
<th translate center field="grouping">Grouping</th>
@ -118,7 +118,7 @@
<tr>
<td center title="{{::line.quantity}}">{{::line.quantity}}</td>
<td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td>
<td center title="{{::line.packageFk | dashIfEmpty}}">{{::line.packageFk | dashIfEmpty}}</td>
<td center title="{{::line.packagingFk | dashIfEmpty}}">{{::line.packagingFk | dashIfEmpty}}</td>
<td center title="{{::line.weight}}">{{::line.weight}}</td>
<td center>
<vn-chip class="transparent" translate-attr="line.groupingMode == 2 ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': line.groupingMode == 2}">

View File

@ -29,44 +29,44 @@ module.exports = Self => {
Object.assign(myOptions, options);
const stmt = new ParameterizedSQL(
`SELECT
w.id AS warehouseFk,
w.name AS warehouse,
tr.landed,
b.id AS buyFk,
b.entryFk,
b.isIgnored,
b.price2,
b.price3,
b.stickers,
b.packing,
b.grouping,
b.groupingMode,
b.weight,
i.stems,
b.quantity,
b.buyingValue +
b.freightValue +
b.comissionValue +
b.packageValue AS cost,
b.buyingValue,
b.freightValue,
b.comissionValue,
b.packageValue,
b.packageFk ,
s.id AS supplierFk,
s.name AS supplier
FROM itemType it
RIGHT JOIN (entry e
LEFT JOIN supplier s ON s.id = e.supplierFk
RIGHT JOIN buy b ON b.entryFk = e.id
LEFT JOIN item i ON i.id = b.itemFk
LEFT JOIN ink ON ink.id = i.inkFk
LEFT JOIN travel tr ON tr.id = e.travelFk
LEFT JOIN warehouse w ON w.id = tr.warehouseInFk
LEFT JOIN origin o ON o.id = i.originFk
) ON it.id = i.typeFk
LEFT JOIN edi.ekt ek ON b.ektFk = ek.id`);
`SELECT w.id AS warehouseFk,
w.name AS warehouse,
tr.landed,
b.id AS buyFk,
b.entryFk,
b.isIgnored,
b.price2,
b.price3,
b.stickers,
b.packing,
b.grouping,
b.groupingMode,
b.weight,
i.stems,
b.quantity,
b.buyingValue +
b.freightValue +
b.comissionValue +
b.packageValue AS cost,
b.buyingValue,
b.freightValue,
b.comissionValue,
b.packageValue,
b.packagingFk ,
s.id AS supplierFk,
s.name AS supplier
FROM itemType it
RIGHT JOIN (entry e
LEFT JOIN supplier s ON s.id = e.supplierFk
RIGHT JOIN buy b ON b.entryFk = e.id
LEFT JOIN item i ON i.id = b.itemFk
LEFT JOIN ink ON ink.id = i.inkFk
LEFT JOIN travel tr ON tr.id = e.travelFk
LEFT JOIN warehouse w ON w.id = tr.warehouseInFk
LEFT JOIN origin o ON o.id = i.originFk
) ON it.id = i.typeFk
LEFT JOIN edi.ekt ek ON b.ektFk = ek.id`
);
stmt.merge(conn.makeSuffix(filter));
return conn.executeStmt(stmt, myOptions);

View File

@ -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": {

View File

@ -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

View File

@ -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

View File

@ -45,7 +45,7 @@
<vn-th field="quantity" number>Quantity</vn-th>
<vn-th number class="expendable">Cost</vn-th>
<vn-th number>Kg.</vn-th>
<vn-th field="packageFk" number>Cube</vn-th>
<vn-th field="packagingFk" number>Cube</vn-th>
<vn-th field="supplierFk" class="expendable">Provider</vn-th>
</vn-tr>
</vn-thead>
@ -94,7 +94,7 @@
</span>
</vn-td>
<vn-td number>{{::entry.weight | dashIfEmpty}}</vn-td>
<vn-td number>{{::entry.packageFk | dashIfEmpty}}</vn-td>
<vn-td number>{{::entry.packagingFk | dashIfEmpty}}</vn-td>
<vn-td class="expendable" title="{{::entry.supplier | dashIfEmpty}}">{{::entry.supplier | dashIfEmpty}}</vn-td>
</vn-tr>
</vn-tbody>

View File

@ -71,7 +71,7 @@ class Controller extends Section {
switch (param) {
case 'id':
case 'quantity':
case 'packageFk':
case 'packagingFk':
return {[`b.${param}`]: value};
case 'supplierFk':
return {[`s.id`]: value};

View File

@ -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">

View File

@ -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

View File

@ -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) {

View File

@ -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>

View File

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

View File

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

View File

@ -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,

View File

@ -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;
}
});
});

View File

@ -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);

View File

@ -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,

View File

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

View File

@ -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');
});
};

View File

@ -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;

View File

@ -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':

View File

@ -201,7 +201,7 @@ class Controller extends Section {
sendImportSms() {
const params = {
ticketId: this.id,
created: this.ticket.updated
shipped: this.ticket.shipped
};
this.showSMSDialog({
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."
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}}"

View File

@ -4,7 +4,7 @@ Show pallet report: Ver hoja de pallet
Change shipped hour: Cambiar 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 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
Make invoice: Crear factura
Regenerate invoice PDF: Regenerar PDF factura

View File

@ -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

View File

@ -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"

View File

@ -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() {

View File

@ -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);
});
});

View File

@ -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

View File

@ -132,12 +132,18 @@ module.exports = Self => {
s.nickname AS cargoSupplierNickname,
s.name AS supplierName,
CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg,
CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) as DECIMAL(10,0)) as volumeKg
CAST(
SUM(
vc.aerealVolumetricDensity *
b.stickers *
IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000
) as DECIMAL(10,0)
) as volumeKg
FROM travel t
LEFT JOIN supplier s ON s.id = t.cargoSupplierFk
LEFT JOIN entry e ON e.travelFk = t.id
LEFT JOIN buy b ON b.entryFk = e.id
LEFT JOIN packaging pkg ON pkg.id = b.packageFk
LEFT JOIN packaging pkg ON pkg.id = b.packagingFk
LEFT JOIN item i ON i.id = b.itemFk
LEFT JOIN itemType it ON it.id = i.typeFk
JOIN warehouse w ON w.id = t.warehouseInFk
@ -169,11 +175,17 @@ module.exports = Self => {
e.evaNotes,
e.invoiceAmount,
CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) as loadedkg,
CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) AS DECIMAL(10,0)) as volumeKg
CAST(
SUM(
vc.aerealVolumetricDensity *
b.stickers *
IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000
) AS DECIMAL(10,0)
) as volumeKg
FROM tmp.travel tr
JOIN entry e ON e.travelFk = tr.id
JOIN buy b ON b.entryFk = e.id
JOIN packaging pkg ON pkg.id = b.packageFk
JOIN packaging pkg ON pkg.id = b.packagingFk
JOIN item i ON i.id = b.itemFk
JOIN itemType it ON it.id = i.typeFk
JOIN supplier s ON s.id = e.supplierFk

View File

@ -47,7 +47,7 @@ module.exports = Self => {
LEFT JOIN vn.buy b ON b.entryFk = e.id
LEFT JOIN vn.supplier s ON e.supplierFk = s.id
JOIN vn.item i ON i.id = b.itemFk
LEFT JOIN vn.packaging p ON p.id = b.packageFk
LEFT JOIN vn.packaging p ON p.id = b.packagingFk
JOIN vn.packaging pcc ON pcc.id = 'cc'
JOIN vn.packaging ppallet ON ppallet.id = 'pallet 100'
JOIN vn.packagingConfig pconfig

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.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]);

View File

@ -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));

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/search')(Self);
require('../methods/worker/isAuthorized')(Self);
require('../methods/worker/setPassword')(Self);
Self.validatesUniquenessOf('locker', {
message: 'This locker has already been assigned'

View File

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

View File

@ -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>

View File

@ -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'];

View File

@ -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');
}
});
});
});

View File

@ -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);
};
};

View File

@ -7,8 +7,8 @@
}
},
"properties": {
"zoneFk": {
"id": true,
"id": {
"id": true,
"type": "number"
},
"isIncluded": {

View File

@ -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>

View File

@ -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 = ?