6287-Ticket.create-no-usarlo #1806

Closed
jgallego wants to merge 3 commits from 6287-Ticket.create-no-usarlo into dev
117 changed files with 2976 additions and 698 deletions
Showing only changes of commit 4d82508645 - Show all commits

View File

@ -26,15 +26,14 @@ module.exports = Self => {
Self.sendCheckingPresence = async(ctx, recipientId, message) => { Self.sendCheckingPresence = async(ctx, recipientId, message) => {
if (!recipientId) return false; if (!recipientId) return false;
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const sender = await models.VnUser.findById(userId, {fields: ['id']}); const sender = await models.VnUser.findById(userId, {fields: ['id']});
const recipient = await models.VnUser.findById(recipientId, null); const recipient = await models.VnUser.findById(recipientId, null);
// Prevent sending messages to yourself // Prevent sending messages to yourself
if (recipientId == userId) return false; if (recipientId == userId) return false;
if (!recipient) if (!recipient)
throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`); throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);

View File

@ -26,7 +26,7 @@ module.exports = Self => {
Self.getTickets = async(ctx, id, print, options) => { Self.getTickets = async(ctx, id, print, options) => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const $t = ctx.req.__; const $t = ctx.req.__;
const myOptions = {}; const myOptions = {};
@ -36,7 +36,6 @@ module.exports = Self => {
myOptions.userId = userId; myOptions.userId = userId;
const promises = []; const promises = [];
const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions); const [tickets] = await Self.rawSql(`CALL vn.collection_getTickets(?)`, [id], myOptions);
const sales = await Self.rawSql(` const sales = await Self.rawSql(`
SELECT s.ticketFk, SELECT s.ticketFk,
@ -86,24 +85,19 @@ module.exports = Self => {
if (tickets && tickets.length) { if (tickets && tickets.length) {
for (const ticket of tickets) { for (const ticket of tickets) {
const ticketId = ticket.ticketFk; const ticketId = ticket.ticketFk;
// SEND ROCKET
if (ticket.observaciones != '') { if (ticket.observaciones != '') {
for (observation of ticket.observaciones.split(' ')) { for (observation of ticket.observaciones.split(' ')) {
if (['#', '@'].includes(observation.charAt(0))) { if (['#', '@'].includes(observation.charAt(0))) {
promises.push(Self.app.models.Chat.send(ctx, observation, promises.push(Self.app.models.Chat.send(ctx, observation,
$t('The ticket is in preparation', { $t('The ticket is in preparation', {
ticketId: ticketId, ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/summary`, ticketUrl: `${url}ticket/${ticketId}/summary`,
salesPersonId: ticket.salesPersonFk salesPersonId: ticket.salesPersonFk
}))); })));
} }
} }
} }
// SET COLLECTION
if (sales && sales.length) { if (sales && sales.length) {
// GET BARCODES
const barcodes = await Self.rawSql(` const barcodes = await Self.rawSql(`
SELECT s.id saleFk, b.code, c.id SELECT s.id saleFk, b.code, c.id
FROM vn.sale s FROM vn.sale s
@ -114,13 +108,10 @@ module.exports = Self => {
WHERE s.ticketFk = ? WHERE s.ticketFk = ?
AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`, AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`,
[ticketId], myOptions); [ticketId], myOptions);
// BINDINGS
ticket.sales = []; ticket.sales = [];
for (const sale of sales) { for (const sale of sales) {
if (sale.ticketFk === ticketId) { if (sale.ticketFk === ticketId) {
sale.Barcodes = []; sale.Barcodes = [];
if (barcodes && barcodes.length) { if (barcodes && barcodes.length) {
for (const barcode of barcodes) { for (const barcode of barcodes) {
if (barcode.saleFk === sale.saleFk) { if (barcode.saleFk === sale.saleFk) {
@ -131,7 +122,6 @@ module.exports = Self => {
} }
} }
} }
ticket.sales.push(sale); ticket.sales.push(sale);
} }
} }
@ -140,7 +130,6 @@ module.exports = Self => {
} }
} }
await Promise.all(promises); await Promise.all(promises);
return collection; return collection;
}; };
}; };

View File

@ -18,6 +18,14 @@ describe('setSaleQuantity()', () => {
it('should change quantity sale', async() => { it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
SELECT 100 as available;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};

View File

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

View File

@ -0,0 +1,30 @@
module.exports = Self => {
Self.remoteMethod('getUrl', {
description: 'Returns the colling app name',
accessType: 'READ',
accepts: [
{
arg: 'app',
type: 'string',
required: false
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/getUrl`,
verb: 'get'
}
});
Self.getUrl = async(appName = 'salix') => {
const {url} = await Self.app.models.Url.findOne({
where: {
appName,
enviroment: process.env.NODE_ENV || 'development'
}
});
return url;
};
};

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

@ -7,17 +7,14 @@ module.exports = Self => {
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
if (!ctx.isNewInstance) return; if (!ctx.isNewInstance) return;
let {message} = ctx.instance; let {message} = ctx.instance;
if (!message) return; if (!message) return;
const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g); const parts = message.match(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
if (!parts) return; if (!parts) return;
const replacedParts = parts.map(part => { const replacedParts = parts.map(part => {
return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, ''); return part.replace(/[!$%^&*()={};':"\\,.<>/?]/g, '');
}); });
for (const [index, part] of parts.entries()) for (const [index, part] of parts.entries())
message = message.replace(part, replacedParts[index]); message = message.replace(part, replacedParts[index]);

View File

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

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

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

View File

@ -1,6 +1,7 @@
const vnModel = require('vn-loopback/common/models/vn-model'); const vnModel = require('vn-loopback/common/models/vn-model');
const LoopBackContext = require('loopback-context');
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
const LoopBackContext = require('loopback-context');
module.exports = function(Self) { module.exports = function(Self) {
vnModel(Self); vnModel(Self);
@ -12,6 +13,7 @@ module.exports = function(Self) {
require('../methods/vn-user/privileges')(Self); require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self); require('../methods/vn-user/renew-token')(Self);
require('../methods/vn-user/update-user')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create'); Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');
@ -90,11 +92,7 @@ module.exports = function(Self) {
}; };
Self.on('resetPasswordRequest', async function(info) { Self.on('resetPasswordRequest', async function(info) {
const loopBackContext = LoopBackContext.getCurrentContext(); const url = await Self.app.models.Url.getUrl();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const defaultHash = '/reset-password?access_token=$token$'; const defaultHash = '/reset-password?access_token=$token$';
const recoverHashes = { const recoverHashes = {
@ -110,7 +108,7 @@ module.exports = function(Self) {
const params = { const params = {
recipient: info.email, recipient: info.email,
lang: user.lang, lang: user.lang,
url: origin + '/#!' + recoverHash url: url.slice(0, -1) + recoverHash
}; };
const options = Object.assign({}, info.options); const options = Object.assign({}, info.options);
@ -178,45 +176,75 @@ module.exports = function(Self) {
Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls Self.sharedClass._methods.find(method => method.name == 'changePassword').ctor.settings.acls
.filter(acl => acl.property != 'changePassword'); .filter(acl => acl.property != 'changePassword');
// FIXME: https://redmine.verdnatura.es/issues/5761 Self.userSecurity = async(ctx, userId, options) => {
// Self.afterRemote('prototype.patchAttributes', async(ctx, instance) => { const models = Self.app.models;
// if (!ctx.args || !ctx.args.data.email) return; const accessToken = ctx?.options?.accessToken || LoopBackContext.getCurrentContext().active.accessToken;
const ctxToken = {req: {accessToken}};
// const loopBackContext = LoopBackContext.getCurrentContext(); if (userId === accessToken.userId) return;
// const httpCtx = {req: loopBackContext.active};
// const httpRequest = httpCtx.req.http.req;
// const headers = httpRequest.headers;
// const origin = headers.origin;
// const url = origin.split(':');
// class Mailer { const myOptions = {};
// async send(verifyOptions, cb) { if (typeof options == 'object')
// const params = { Object.assign(myOptions, options);
// url: verifyOptions.verifyHref,
// recipient: verifyOptions.to,
// lang: ctx.req.getLocale()
// };
// const email = new Email('email-verify', params); const hasHigherPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'higherPrivileges', myOptions);
// email.send(); if (hasHigherPrivileges) return;
// cb(null, verifyOptions.to); const hasMediumPrivileges = await models.ACL.checkAccessAcl(ctxToken, 'VnUser', 'mediumPrivileges', myOptions);
// } const user = await models.VnUser.findById(userId, {fields: ['id', 'emailVerified']}, myOptions);
// } if (!user.emailVerified && hasMediumPrivileges) return;
// const options = { throw new ForbiddenError();
// type: 'email', };
// to: instance.email,
// from: {},
// 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
// };
// 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,15 +13,12 @@
"type": "number", "type": "number",
"id": true "id": true
}, },
"name": { "name": {
"type": "string", "type": "string",
"required": true "required": true
}, },
"username": { "username": {
"type": "string", "type": "string"
"mysql": {
"columnName": "name"
}
}, },
"roleFk": { "roleFk": {
"type": "number", "type": "number",
@ -38,6 +35,12 @@
"active": { "active": {
"type": "boolean" "type": "boolean"
}, },
"email": {
"type": "string"
},
"emailVerified": {
"type": "boolean"
},
"created": { "created": {
"type": "date" "type": "date"
}, },
@ -137,7 +140,8 @@
"image", "image",
"hasGrant", "hasGrant",
"realm", "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), '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'); ('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 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)), (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)), (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`) INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES VALUES
('lilium', 'development', 'http://localhost:9000/#/'), ('lilium', 'development', 'http://localhost:9000/#/'),
('hedera', 'development', 'http://localhost:9090/'),
('salix', 'development', 'http://localhost:5000/#!/'); ('salix', 'development', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`) INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)

View File

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

View File

@ -53,7 +53,8 @@ describe('ticket ticketCalculateClon()', () => {
expect(result[orderIndex][0].ticketFk).toBeGreaterThan(newestTicketIdInFixtures); expect(result[orderIndex][0].ticketFk).toBeGreaterThan(newestTicketIdInFixtures);
}); });
it('should add the ticket to the order containing the original ticket and generate landed value if it was null', async() => { it('should add the ticket to the order containing the original ' +
'ticket and generate landed value if it was null', async() => {
let stmts = []; let stmts = [];
let stmt; let stmt;

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"]', 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"]', 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"]', 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"]', 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"]', 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"]', importButton: 'vn-entry-buy-index vn-icon[icon="publish"]',

View File

@ -134,14 +134,6 @@ describe('Ticket Edit sale path', () => {
await page.accessToSection('ticket.card.sale'); await page.accessToSection('ticket.card.sale');
}); });
it('should try to add a higher quantity value and then receive an error', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell);
await page.type(selectors.ticketSales.firstSaleQuantity, '11\u000d');
const message = await page.waitForSnackbar();
expect(message.text).toContain('The new quantity should be smaller than the old one');
});
it('should remove 1 from the first sale quantity', async() => { it('should remove 1 from the first sale quantity', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell); await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell);
await page.waitForSelector(selectors.ticketSales.firstSaleQuantity); await page.waitForSelector(selectors.ticketSales.firstSaleQuantity);

View File

@ -1,20 +1,5 @@
import getBrowser from '../../helpers/puppeteer'; 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', () => { describe('Supplier fiscal data path', () => {
let browser; let browser;
let page; let page;
@ -30,7 +15,7 @@ describe('Supplier fiscal data path', () => {
await browser.close(); 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'); await page.accessToSection('supplier.card.fiscalData');
const form = 'vn-supplier-fiscal-data form'; const form = 'vn-supplier-fiscal-data form';
@ -40,16 +25,16 @@ describe('Supplier fiscal data path', () => {
postcode: null, postcode: null,
city: 'Valencia', city: 'Valencia',
socialName: 'Farmer King SL', socialName: 'Farmer King SL',
taxNumber: 'Wrong tax number', taxNumber: '12345678Z',
account: '0123456789', account: '0123456789',
sageWithholding: 'retencion estimacion objetiva', sageWithholding: 'retencion estimacion objetiva',
sageTaxType: 'operaciones no sujetas' sageTaxType: 'operaciones no sujetas'
}; };
const errorMessage = await page.sendForm(form, values); const errorMessage = await page.sendForm(form, {
const message = await page.sendForm(form, { taxNumber: 'Wrong tax number'
taxNumber: '12345678Z'
}); });
const message = await page.sendForm(form, values);
await page.reloadSection('supplier.card.fiscalData'); await page.reloadSection('supplier.card.fiscalData');
const formValues = await page.fetchForm(form, Object.keys(values)); const formValues = await page.fetchForm(form, Object.keys(values));

View File

@ -13,6 +13,6 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
} }
] ]
} }

View File

@ -14,7 +14,7 @@
"The default consignee can not be unchecked": "The default consignee can not be unchecked", "The default consignee can not be unchecked": "The default consignee can not be unchecked",
"Enter an integer different to zero": "Enter an integer different to zero", "Enter an integer different to zero": "Enter an integer different to zero",
"Package cannot be blank": "Package cannot be blank", "Package cannot be blank": "Package cannot be blank",
"The new quantity should be smaller than the old one": "The new quantity should be smaller than the old one", "The price of the item changed": "The price of the item changed",
"The sales of this ticket can't be modified": "The sales of this ticket can't be modified", "The sales of this ticket can't be modified": "The sales of this ticket can't be modified",
"Cannot check Equalization Tax in this NIF/CIF": "Cannot check Equalization Tax in this NIF/CIF", "Cannot check Equalization Tax in this NIF/CIF": "Cannot check Equalization Tax in this NIF/CIF",
"You can't create an order for a frozen client": "You can't create an order for a frozen client", "You can't create an order for a frozen client": "You can't create an order for a frozen client",
@ -189,5 +189,6 @@
"The sales do not exists": "The sales do not exists", "The sales do not exists": "The sales do not exists",
"Ticket without Route": "Ticket without route", "Ticket without Route": "Ticket without route",
"Booking completed": "Booking complete", "Booking completed": "Booking complete",
"The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation" "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
"You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets"
} }

View File

@ -35,7 +35,7 @@
"The grade must be an integer greater than or equal to zero": "El grade debe ser un entero mayor o igual a cero", "The grade must be an integer greater than or equal to zero": "El grade debe ser un entero mayor o igual a cero",
"Sample type cannot be blank": "El tipo de plantilla no puede quedar en blanco", "Sample type cannot be blank": "El tipo de plantilla no puede quedar en blanco",
"Description cannot be blank": "Se debe rellenar el campo de texto", "Description cannot be blank": "Se debe rellenar el campo de texto",
"The new quantity should be smaller than the old one": "La nueva cantidad debe de ser menor que la anterior", "The price of the item changed": "El precio del artículo cambió",
"The value should not be greater than 100%": "El valor no debe de ser mayor de 100%", "The value should not be greater than 100%": "El valor no debe de ser mayor de 100%",
"The value should be a number": "El valor debe ser un numero", "The value should be a number": "El valor debe ser un numero",
"This order is not editable": "Esta orden no se puede modificar", "This order is not editable": "Esta orden no se puede modificar",
@ -320,5 +320,7 @@
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta", "Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación" "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina"
} }

View File

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

View File

@ -1,9 +1,9 @@
<mg-ajax path="VnUsers/{{patch.params.id}}/update-user" options="vnPatch"></mg-ajax>
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="VnUsers"
data="$ctrl.user" data="$ctrl.user"
id-value="$ctrl.$params.id" form="form"
form="form"> save="patch">
</vn-watcher> </vn-watcher>
<form <form
name="form" name="form"
@ -12,18 +12,18 @@
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-vertical> <vn-vertical>
<vn-textfield <vn-textfield
label="User" label="User"
ng-model="$ctrl.user.name" ng-model="$ctrl.user.name"
rule="VnUser" rule="VnUser"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Nickname" label="Nickname"
ng-model="$ctrl.user.nickname" ng-model="$ctrl.user.nickname"
rule="VnUser"> rule="VnUser">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Personal email" label="Personal email"
ng-model="$ctrl.user.email" ng-model="$ctrl.user.email"
rule="VnUser"> rule="VnUser">
</vn-textfield> </vn-textfield>

View File

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

View File

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

View File

@ -43,9 +43,8 @@ module.exports = Self => {
Self.claimPickupEmail = async ctx => { Self.claimPickupEmail = async ctx => {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const args = Object.assign({}, ctx.args); const args = Object.assign({}, ctx.args);
const params = { const params = {
@ -70,9 +69,8 @@ module.exports = Self => {
const message = $t('Claim pickup order sent', { const message = $t('Claim pickup order sent', {
claimId: args.id, claimId: args.id,
clientName: claim.client().name, clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${args.id}/summary`, claimUrl: `${url}claim/${args.id}/summary`,
}); });
const salesPersonId = claim.client().salesPersonFk; const salesPersonId = claim.client().salesPersonFk;
if (salesPersonId) if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);

View File

@ -94,13 +94,13 @@ module.exports = Self => {
const salesPerson = ticket.client().salesPersonUser(); const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Created claim', { const message = $t('Created claim', {
claimId: newClaim.id, claimId: newClaim.id,
ticketId: ticketId, ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, ticketUrl: `${url}ticket/${ticketId}/sale`,
claimUrl: `${origin}/#!/claim/${newClaim.id}/summary`, claimUrl: `${url}claim/${newClaim.id}/summary`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);

View File

@ -56,15 +56,15 @@ module.exports = Self => {
const salesPerson = sale.ticket().client().salesPersonUser(); const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const nickname = address && address.nickname || destination.description; const nickname = address && address.nickname || destination.description;
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Sent units from ticket', { const message = $t('Sent units from ticket', {
quantity: sale.quantity, quantity: sale.quantity,
concept: sale.concept, concept: sale.concept,
itemId: sale.itemFk, itemId: sale.itemFk,
ticketId: sale.ticketFk, ticketId: sale.ticketFk,
nickname: nickname, nickname: nickname,
ticketUrl: `${origin}/#!/ticket/${sale.ticketFk}/sale`, ticketUrl: `${url}ticket/${sale.ticketFk}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` itemUrl: `${url}item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
} }

View File

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

View File

@ -2,7 +2,9 @@ const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('Update Claim', () => { describe('Update Claim', () => {
let url;
beforeAll(async() => { beforeAll(async() => {
url = await app.models.Url.getUrl();
const activeCtx = { const activeCtx = {
accessToken: {userId: 9}, accessToken: {userId: 9},
http: { http: {
@ -29,7 +31,6 @@ describe('Update Claim', () => {
it(`should throw an error as the user doesn't have rights`, async() => { it(`should throw an error as the user doesn't have rights`, async() => {
const tx = await app.models.Claim.beginTransaction({}); const tx = await app.models.Claim.beginTransaction({});
let error; let error;
try { try {
@ -77,7 +78,7 @@ describe('Update Claim', () => {
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: claimManagerId}, accessToken: {userId: claimManagerId},
headers: {origin: 'http://localhost'} headers: {origin: url}
}, },
args: { args: {
observation: 'valid observation', observation: 'valid observation',
@ -118,7 +119,7 @@ describe('Update Claim', () => {
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: claimManagerId}, accessToken: {userId: claimManagerId},
headers: {origin: 'http://localhost'} headers: {origin: url}
}, },
args: { args: {
observation: 'valid observation', observation: 'valid observation',

View File

@ -91,16 +91,16 @@ module.exports = Self => {
// When hasToPickUp has been changed // When hasToPickUp has been changed
if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp) if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp)
notifyPickUp(ctx, salesPerson.id, claim); await notifyPickUp(ctx, salesPerson.id, claim);
// When claimState has been changed // When claimState has been changed
if (args.claimStateFk) { if (args.claimStateFk) {
const newState = await models.ClaimState.findById(args.claimStateFk, null, myOptions); const newState = await models.ClaimState.findById(args.claimStateFk, null, myOptions);
if (newState.hasToNotify) { if (newState.hasToNotify) {
if (newState.code == 'incomplete') if (newState.code == 'incomplete')
notifyStateChange(ctx, salesPerson.id, claim, newState.code); await notifyStateChange(ctx, salesPerson.id, claim, newState.code);
if (newState.code == 'canceled') if (newState.code == 'canceled')
notifyStateChange(ctx, claim.workerFk, claim, newState.code); await notifyStateChange(ctx, claim.workerFk, claim, newState.code);
} }
} }
@ -115,26 +115,26 @@ module.exports = Self => {
async function notifyStateChange(ctx, workerId, claim, state) { async function notifyStateChange(ctx, workerId, claim, state) {
const models = Self.app.models; const models = Self.app.models;
const origin = ctx.req.headers.origin; const url = await models.Url.getUrl();
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const message = $t(`Claim state has changed to ${state}`, { const message = $t(`Claim state has changed to ${state}`, {
claimId: claim.id, claimId: claim.id,
clientName: claim.client().name, clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary` claimUrl: `${url}claim/${claim.id}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, workerId, message); await models.Chat.sendCheckingPresence(ctx, workerId, message);
} }
async function notifyPickUp(ctx, workerId, claim) { async function notifyPickUp(ctx, workerId, claim) {
const origin = ctx.req.headers.origin;
const models = Self.app.models; const models = Self.app.models;
const url = await models.Url.getUrl();
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const message = $t('Claim will be picked', { const message = $t('Claim will be picked', {
claimId: claim.id, claimId: claim.id,
clientName: claim.client().name, clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary` claimUrl: `${url}claim/${claim.id}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, workerId, message); await models.Chat.sendCheckingPresence(ctx, workerId, message);
} }

View File

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

View File

@ -250,7 +250,12 @@ module.exports = Self => {
const loopBackContext = LoopBackContext.getCurrentContext(); const loopBackContext = LoopBackContext.getCurrentContext();
const accessToken = {req: loopBackContext.active.accessToken}; const accessToken = {req: loopBackContext.active.accessToken};
const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl(accessToken, 'Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE'); const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl(
accessToken,
'Client',
'editVerifiedDataWithoutTaxDataCheck',
'WRITE'
);
const hasChanges = orgData && changes; const hasChanges = orgData && changes;
const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);
@ -263,7 +268,9 @@ module.exports = Self => {
const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType;
const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked; const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked;
const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !editVerifiedDataWithoutTaxDataChecked; const cantChangeSageData = (sageTaxTypeChanged ||
sageTransactionTypeChanged
) && !editVerifiedDataWithoutTaxDataChecked;
if (cantEditVerifiedData || cantChangeSageData) if (cantEditVerifiedData || cantChangeSageData)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
@ -346,8 +353,7 @@ module.exports = Self => {
const httpCtx = {req: loopBackContext.active}; const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req; const httpRequest = httpCtx.req.http.req;
const $t = httpRequest.__; const $t = httpRequest.__;
const headers = httpRequest.headers; const url = await Self.app.models.Url.getUrl();
const origin = headers.origin;
const salesPersonId = instance.salesPersonFk; const salesPersonId = instance.salesPersonFk;
@ -366,7 +372,7 @@ module.exports = Self => {
await email.send(); await email.send();
} }
const fullUrl = `${origin}/#!/client/${instance.id}/billing-data`; const fullUrl = `${url}client/${instance.id}/billing-data`;
const message = $t('Changed client paymethod', { const message = $t('Changed client paymethod', {
clientId: instance.id, clientId: instance.id,
clientName: instance.name, clientName: instance.name,
@ -389,8 +395,7 @@ module.exports = Self => {
const httpCtx = {req: loopBackContext.active}; const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req; const httpRequest = httpCtx.req.http.req;
const $t = httpRequest.__; const $t = httpRequest.__;
const headers = httpRequest.headers; const url = await Self.app.models.Url.getUrl();
const origin = headers.origin;
const models = Self.app.models; const models = Self.app.models;
let previousWorker = {name: $t('None')}; let previousWorker = {name: $t('None')};
@ -411,7 +416,7 @@ module.exports = Self => {
currentWorker.name = worker && worker.user().nickname; currentWorker.name = worker && worker.user().nickname;
} }
const fullUrl = `${origin}/#!/client/${client.id}/basic-data`; const fullUrl = `${url}client/${client.id}/basic-data`;
const message = $t('Client assignment has changed', { const message = $t('Client assignment has changed', {
clientId: client.id, clientId: client.id,
clientName: client.name, clientName: client.name,

View File

@ -57,8 +57,8 @@ module.exports = function(Self) {
const httpRequest = httpCtx.req.http.req; const httpRequest = httpCtx.req.http.req;
const $t = httpRequest.__; const $t = httpRequest.__;
const origin = httpRequest.headers.origin; const url = await Self.app.models.Url.getUrl();
const fullPath = `${origin}/#!/client/${client.id}/credit-insurance/index`; const fullPath = `${url}client/${client.id}/credit-insurance/index`;
const message = $t('MESSAGE_INSURANCE_CHANGE', { const message = $t('MESSAGE_INSURANCE_CHANGE', {
clientId: client.id, clientId: client.id,
clientName: client.name, clientName: client.name,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ describe('entry import()', () => {
packing: 1, packing: 1,
size: 1, size: 1,
volume: 1200, volume: 1200,
packageFk: '94' packagingFk: '94'
}, },
{ {
itemFk: 4, itemFk: 4,
@ -43,7 +43,7 @@ describe('entry import()', () => {
packing: 1, packing: 1,
size: 25, size: 25,
volume: 1125, 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 tx = await models.Entry.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};
try { try {
const expectedPackageFk = '3'; const expectedPackagingFk = '3';
const buys = [ const buys = [
{ {
itemFk: 1, itemFk: 1,
@ -39,7 +39,7 @@ describe('entry importBuysPreview()', () => {
const randomIndex = Math.floor(Math.random() * result.length); const randomIndex = Math.floor(Math.random() * result.length);
const buy = result[randomIndex]; const buy = result[randomIndex];
expect(buy.packageFk).toEqual(expectedPackageFk); expect(buy.packagingFk).toEqual(expectedPackagingFk);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ describe('Entry buy', () => {
describe('saveBuy()', () => { describe('saveBuy()', () => {
it(`should call the buys patch route if the received buy has an ID`, () => { 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}`; const query = `Buys/${buy.id}`;

View File

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

View File

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

View File

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

View File

@ -59,10 +59,10 @@ module.exports = Self => {
}; };
await Self.invoiceEmail(ctx, ref); await Self.invoiceEmail(ctx, ref);
} catch (err) { } catch (err) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = ctx.req.__('Mail not sent', { const message = ctx.req.__('Mail not sent', {
clientId: client.id, clientId: client.id,
clientUrl: `${origin}/#!/claim/${id}/summary` clientUrl: `${url}claim/${id}/summary`
}); });
const salesPersonId = client.salesPersonFk; const salesPersonId = client.salesPersonFk;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@
<vn-th field="ticketFk" number>Ticket ID</vn-th> <vn-th field="ticketFk" number>Ticket ID</vn-th>
<vn-th field="shipped" expand>Shipped</vn-th> <vn-th field="shipped" expand>Shipped</vn-th>
<vn-th field="description" filter-enabled="false" expand>Description</vn-th> <vn-th field="description" filter-enabled="false" expand>Description</vn-th>
<vn-th field="requesterFk" >Requester</vn-th>
<vn-th field="quantity" number editable>Requested</vn-th> <vn-th field="quantity" number editable>Requested</vn-th>
<vn-th field="price" number>Price</vn-th> <vn-th field="price" number>Price</vn-th>
<vn-th field="attenderName">Atender</vn-th> <vn-th field="attenderName">Atender</vn-th>
@ -51,6 +52,13 @@
</span> </span>
</vn-td> </vn-td>
<vn-td title="{{::request.description}}" expand>{{::request.description}}</vn-td> <vn-td title="{{::request.description}}" expand>{{::request.description}}</vn-td>
<vn-td number>
<span
class="link"
ng-click="workerDescriptor.show($event, request.requesterFk)">
{{::request.requesterName}}
</span>
</vn-td>
<vn-td number>{{::request.quantity}}</vn-td> <vn-td number>{{::request.quantity}}</vn-td>
<vn-td number>{{::request.price | currency: 'EUR':2}}</vn-td> <vn-td number>{{::request.price | currency: 'EUR':2}}</vn-td>
<vn-td> <vn-td>

View File

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

View File

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

View File

@ -100,31 +100,32 @@ module.exports = Self => {
)); ));
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT SELECT i.id,
i.id, i.name,
i.name, i.subName,
i.subName, i.image,
i.image, i.tag5,
i.tag5, i.value5,
i.value5, i.tag6,
i.tag6, i.value6,
i.value6, i.tag7,
i.tag7, i.value7,
i.value7, i.tag8,
i.tag8, i.value8,
i.value8, i.stars,
i.stars, tci.price,
tci.price, tci.available,
tci.available, w.lastName,
w.lastName AS lastName, w.firstName,
w.firstName, tci.priceKg,
tci.priceKg, ink.hex,
ink.hex i.minQuantity
FROM tmp.ticketCalculateItem tci FROM tmp.ticketCalculateItem tci
JOIN vn.item i ON i.id = tci.itemFk JOIN vn.item i ON i.id = tci.itemFk
JOIN vn.itemType it ON it.id = i.typeFk JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.worker w on w.id = it.workerFk JOIN vn.worker w on w.id = it.workerFk
LEFT JOIN vn.ink ON ink.id = i.inkFk`); LEFT JOIN vn.ink ON ink.id = i.inkFk
`);
// Apply order by tag // Apply order by tag
if (orderBy.isTag) { if (orderBy.isTag) {

View File

@ -8,12 +8,12 @@
<div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div> <div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div> </div>
<img <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)}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/> on-error-src/>
</div> </div>
<div class="description"> <div class="description">
<h3 class="link" <h3 class="link"
ng-click="itemDescriptor.show($event, item.id)"> ng-click="itemDescriptor.show($event, item.id)">
{{::item.name}} {{::item.name}}
</h3> </h3>
@ -37,13 +37,28 @@
value="{{::item.value7}}"> value="{{::item.value7}}">
</vn-label-value> </vn-label-value>
</div> </div>
<vn-rating ng-if="::item.stars" <vn-horizontal>
ng-model="::item.stars"> <vn-one>
</vn-rating> <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="footer">
<div class="price"> <div class="price">
<vn-one> <vn-one>
<span>{{::item.available}}</span> <span>{{::item.available}}</span>
<span translate>to</span> <span translate>to</span>
<span>{{::item.price | currency:'EUR':2}}</span> <span>{{::item.price | currency:'EUR':2}}</span>
</vn-one> </vn-one>
@ -54,7 +69,7 @@
</vn-icon-button> </vn-icon-button>
</div> </div>
<div class="priceKg" ng-show="::item.priceKg"> <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> </div>
</div> </div>
@ -69,4 +84,4 @@
<vn-item-descriptor-popover <vn-item-descriptor-popover
vn-id="item-descriptor" vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk"> warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover> </vn-item-descriptor-popover>

View File

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

View File

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

View File

@ -1,5 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('deleteSales', { Self.remoteMethodCtx('deleteSales', {
description: 'Deletes the selected sales', description: 'Deletes the selected sales',
@ -70,11 +68,11 @@ module.exports = Self => {
const salesPerson = ticket.client().salesPersonUser(); const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Deleted sales from ticket', { const message = $t('Deleted sales from ticket', {
ticketId: ticketId, ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, ticketUrl: `${url}ticket/${ticketId}/sale`,
deletions: deletions deletions: deletions
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);

View File

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

View File

@ -1,6 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('reserve', { Self.remoteMethodCtx('reserve', {
description: 'Change the state of a ticket', description: 'Change the state of a ticket',
@ -65,7 +62,8 @@ module.exports = Self => {
promises.push(reservedSale); promises.push(reservedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`; changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})
${$t('State')}: ${$t(oldState)} *${$t(newState)}*`;
} }
} }
@ -87,11 +85,11 @@ module.exports = Self => {
const salesPerson = ticket.client().salesPersonUser(); const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Changed sale reserved state', { const message = $t('Changed sale reserved state', {
ticketId: ticketId, ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, ticketUrl: `${url}ticket/${ticketId}/sale`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);

View File

@ -1,162 +0,0 @@
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},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
it('should throw an error if the quantity is greater than it should be', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, 17, 99, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('The new quantity should be smaller than the old one'));
});
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 = {
req: {
accessToken: {userId: buyerId},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
try {
const options = {transaction: tx};
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
expect(isRoleAdvanced).toEqual(true);
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(30);
const newQuantity = originalLine.quantity + 1;
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should update the quantity of a given sale current line', async() => {
const tx = await models.Sale.beginTransaction({});
const saleId = 25;
const newQuantity = 4;
try {
const options = {transaction: tx};
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(20);
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an error if the quantity is negative and it is not a refund ticket', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const saleId = 17;
const newQuantity = -10;
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('You can only add negative amounts in refund tickets'));
});
it('should update a negative quantity when is a ticket refund', async() => {
const tx = await models.Sale.beginTransaction({});
const saleId = 13;
const newQuantity = -10;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -1,5 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updatePrice', { Self.remoteMethodCtx('updatePrice', {
description: 'Changes the price of a sale', description: 'Changes the price of a sale',
@ -100,7 +98,7 @@ module.exports = Self => {
const salesPerson = sale.ticket().client().salesPersonUser(); const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Changed sale price', { const message = $t('Changed sale price', {
ticketId: sale.ticket().id, ticketId: sale.ticket().id,
itemId: sale.itemFk, itemId: sale.itemFk,
@ -108,8 +106,8 @@ module.exports = Self => {
quantity: sale.quantity, quantity: sale.quantity,
oldPrice: oldPrice, oldPrice: oldPrice,
newPrice: newPrice, newPrice: newPrice,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, ticketUrl: `${url}ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` itemUrl: `${url}item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
} }

View File

@ -1,4 +1,3 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateQuantity', { Self.remoteMethodCtx('updateQuantity', {
@ -64,32 +63,22 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions); const sale = await models.Sale.findById(id, filter, myOptions);
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
if (newQuantity > sale.quantity && !isRoleAdvanced)
throw new UserError('The new quantity should be smaller than the old one');
const ticketRefund = await models.TicketRefund.findOne({
where: {refundTicketFk: sale.ticketFk},
fields: ['id']}
, myOptions);
if (newQuantity < 0 && !ticketRefund)
throw new UserError('You can only add negative amounts in refund tickets');
const oldQuantity = sale.quantity; const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
const salesPerson = sale.ticket().client().salesPersonUser(); const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Changed sale quantity', { const message = $t('Changed sale quantity', {
ticketId: sale.ticket().id, ticketId: sale.ticket().id,
itemId: sale.itemFk, itemId: sale.itemFk,
concept: sale.concept, concept: sale.concept,
oldQuantity: oldQuantity, oldQuantity: oldQuantity,
newQuantity: newQuantity, newQuantity: newQuantity,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, ticketUrl: `${url}ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` itemUrl: `${url}item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
} }

View File

@ -84,7 +84,7 @@ module.exports = Self => {
const query = `CALL vn.sale_calculateComponent(?, NULL)`; const query = `CALL vn.sale_calculateComponent(?, NULL)`;
await Self.rawSql(query, [sale.id], myOptions); await Self.rawSql(query, [sale.id], myOptions);
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const requesterId = request.requesterFk; const requesterId = request.requesterFk;
const message = $t('Bought units from buy request', { const message = $t('Bought units from buy request', {
@ -92,8 +92,8 @@ module.exports = Self => {
concept: sale.concept, concept: sale.concept,
itemId: sale.itemFk, itemId: sale.itemFk,
ticketId: sale.ticketFk, ticketId: sale.ticketFk,
url: `${origin}/#!/ticket/${sale.ticketFk}/summary`, url: `${url}ticket/${sale.ticketFk}/summary`,
urlItem: `${origin}/#!/item/${sale.itemFk}/summary` urlItem: `${url}item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, requesterId, message, myOptions); await models.Chat.sendCheckingPresence(ctx, requesterId, message, myOptions);

View File

@ -50,12 +50,12 @@ module.exports = Self => {
const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions); const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions);
await request.updateAttributes(params, myOptions); await request.updateAttributes(params, myOptions);
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const requesterId = request.requesterFk; const requesterId = request.requesterFk;
const message = $t('Deny buy request', { const message = $t('Deny buy request', {
ticketId: request.ticketFk, ticketId: request.ticketFk,
url: `${origin}/#!/ticket/${request.ticketFk}/request/index`, url: `${url}ticket/${request.ticketFk}/request/index`,
observation: params.response observation: params.response
}); });

View File

@ -99,6 +99,8 @@ module.exports = Self => {
switch (value) { switch (value) {
case 'pending': case 'pending':
return {'tr.isOk': null}; return {'tr.isOk': null};
case 'accepted':
return {'tr.isOk': 1};
default: default:
return {'tr.isOk': value}; return {'tr.isOk': value};
} }
@ -122,8 +124,7 @@ module.exports = Self => {
filter = mergeFilters(filter, {where}); filter = mergeFilters(filter, {where});
const stmt = new ParameterizedSQL( const stmt = new ParameterizedSQL(
`SELECT `SELECT tr.id,
tr.id,
tr.ticketFk, tr.ticketFk,
tr.quantity, tr.quantity,
tr.price, tr.price,
@ -133,18 +134,19 @@ module.exports = Self => {
tr.saleFk, tr.saleFk,
tr.requesterFk, tr.requesterFk,
tr.isOk, tr.isOk,
s.quantity AS saleQuantity, s.quantity saleQuantity,
s.itemFk, s.itemFk,
i.name AS itemDescription, i.name itemDescription,
t.shipped, t.shipped,
DATE(t.shipped) AS shippedDate, DATE(t.shipped) shippedDate,
t.nickname, t.nickname,
t.warehouseFk, t.warehouseFk,
t.clientFk, t.clientFk,
w.name AS warehouse, w.name warehouse,
u.nickname AS salesPersonNickname, u.nickname salesPersonNickname,
ua.name AS attenderName, ua.name attenderName,
c.salesPersonFk c.salesPersonFk,
ua2.name requesterName
FROM ticketRequest tr FROM ticketRequest tr
LEFT JOIN ticketWeekly tw on tw.ticketFk = tr.ticketFk LEFT JOIN ticketWeekly tw on tw.ticketFk = tr.ticketFk
LEFT JOIN ticket t ON t.id = tr.ticketFk LEFT JOIN ticket t ON t.id = tr.ticketFk
@ -155,7 +157,8 @@ module.exports = Self => {
LEFT JOIN worker wk ON wk.id = c.salesPersonFk LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.id LEFT JOIN account.user u ON u.id = wk.id
LEFT JOIN worker wka ON wka.id = tr.attenderFk LEFT JOIN worker wka ON wka.id = tr.attenderFk
LEFT JOIN account.user ua ON ua.id = wka.id`); LEFT JOIN account.user ua ON ua.id = wka.id
LEFT JOIN account.user ua2 ON ua2.id = tr.requesterFk`);
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
return conn.executeStmt(stmt, myOptions); return conn.executeStmt(stmt, myOptions);

View File

@ -63,17 +63,6 @@ module.exports = Self => {
} }
}, myOptions); }, myOptions);
const itemInfo = await models.Item.getVisibleAvailable(
itemId,
ticket.warehouseFk,
ticket.shipped,
myOptions
);
const isPackaging = item.family == 'EMB';
if (!isPackaging && itemInfo.available < quantity)
throw new UserError(`This item is not available`);
const newSale = await models.Sale.create({ const newSale = await models.Sale.create({
ticketFk: id, ticketFk: id,
itemFk: item.id, itemFk: item.id,
@ -94,11 +83,11 @@ module.exports = Self => {
const salesPerson = ticket.client().salesPersonUser(); const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Added sale to ticket', { const message = $t('Added sale to ticket', {
ticketId: id, ticketId: id,
ticketUrl: `${origin}/#!/ticket/${id}/sale`, ticketUrl: `${url}ticket/${id}/sale`,
addition: addition addition: addition
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);

View File

@ -237,7 +237,7 @@ module.exports = Self => {
const salesPersonId = originalTicket.client().salesPersonFk; const salesPersonId = originalTicket.client().salesPersonFk;
if (salesPersonId) { if (salesPersonId) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
let changesMade = ''; let changesMade = '';
for (let change in newProperties) { for (let change in newProperties) {
@ -249,7 +249,7 @@ module.exports = Self => {
const message = $t('Changed this data from the ticket', { const message = $t('Changed this data from the ticket', {
ticketId: args.id, ticketId: args.id,
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`, ticketUrl: `${url}ticket/${args.id}/sale`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);

View File

@ -25,7 +25,7 @@ module.exports = Self => {
Self.merge = async(ctx, tickets, options) => { Self.merge = async(ctx, tickets, options) => {
const httpRequest = ctx.req; const httpRequest = ctx.req;
const $t = httpRequest.__; const $t = httpRequest.__;
const origin = httpRequest.headers.origin; const url = await Self.app.models.Url.getUrl();
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -40,8 +40,8 @@ module.exports = Self => {
try { try {
for (let ticket of tickets) { for (let ticket of tickets) {
const originFullPath = `${origin}/#!/ticket/${ticket.originId}/summary`; const originFullPath = `${url}ticket/${ticket.originId}/summary`;
const destinationFullPath = `${origin}/#!/ticket/${ticket.destinationId}/summary`; const destinationFullPath = `${url}ticket/${ticket.destinationId}/summary`;
const message = $t('Ticket merged', { const message = $t('Ticket merged', {
originDated: dateUtil.toString(new Date(ticket.originShipped)), originDated: dateUtil.toString(new Date(ticket.originShipped)),
destinationDated: dateUtil.toString(new Date(ticket.destinationShipped)), destinationDated: dateUtil.toString(new Date(ticket.destinationShipped)),

View File

@ -48,10 +48,10 @@ module.exports = Self => {
// Send notification to salesPerson // Send notification to salesPerson
const salesPersonId = ticket.client().salesPersonFk; const salesPersonId = ticket.client().salesPersonFk;
if (salesPersonId) { if (salesPersonId) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t(`I have restored the ticket id`, { const message = $t(`I have restored the ticket id`, {
id: id, id: id,
url: `${origin}/#!/ticket/${id}/summary` url: `${url}ticket/${id}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
} }

View File

@ -119,10 +119,10 @@ module.exports = Self => {
// Send notification to salesPerson // Send notification to salesPerson
const salesPersonUser = ticket.client().salesPersonUser(); const salesPersonUser = ticket.client().salesPersonUser();
if (salesPersonUser && sales.length) { if (salesPersonUser && sales.length) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t(`I have deleted the ticket id`, { const message = $t(`I have deleted the ticket id`, {
id: id, id: id,
url: `${origin}/#!/ticket/${id}/summary` url: `${url}ticket/${id}/summary`
}); });
await models.Chat.send(ctx, `@${salesPersonUser.name}`, message); await models.Chat.send(ctx, `@${salesPersonUser.name}`, message);
} }
@ -146,7 +146,8 @@ module.exports = Self => {
JOIN vn.sectorCollection sc ON sc.id = scsg.sectorCollectionFk JOIN vn.sectorCollection sc ON sc.id = scsg.sectorCollectionFk
JOIN vn.saleGroupDetail sgd ON sgd.saleGroupFk = sg.id JOIN vn.saleGroupDetail sgd ON sgd.saleGroupFk = sg.id
JOIN vn.sale s ON s.id = sgd.saleFk JOIN vn.sale s ON s.id = sgd.saleFk
WHERE s.ticketFk = ?;`, [ticket.id], myOptions); WHERE s.ticketFk = ?;`, [ticket.id], myOptions
);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -165,11 +165,10 @@ module.exports = Self => {
const salesPerson = ticket.client().salesPersonUser(); const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
const origin = ctx.req.headers.origin; const url = await Self.app.models.Url.getUrl();
const message = $t('Changed sale discount', { const message = $t('Changed sale discount', {
ticketId: id, ticketId: id,
ticketUrl: `${origin}/#!/ticket/${id}/sale`, ticketUrl: `${url}ticket/${id}/sale`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);

View File

@ -1,3 +1,6 @@
const UserError = require('vn-loopback/util/user-error');
const LoopBackContext = require('loopback-context');
module.exports = Self => { module.exports = Self => {
require('../methods/sale/getClaimableFromTicket')(Self); require('../methods/sale/getClaimableFromTicket')(Self);
require('../methods/sale/reserve')(Self); require('../methods/sale/reserve')(Self);
@ -13,4 +16,100 @@ module.exports = Self => {
Self.validatesPresenceOf('concept', { Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank` message: `Concept cannot be blank`
}); });
Self.observe('before save', async ctx => {
const models = Self.app.models;
const changes = ctx.data || ctx.instance;
const instance = ctx.currentInstance;
const newQuantity = changes?.quantity;
if (newQuantity == null) return;
const loopBackContext = LoopBackContext.getCurrentContext();
ctx.req = loopBackContext.active;
if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'canForceQuantity', 'WRITE')) return;
const ticketId = changes?.ticketFk || instance?.ticketFk;
const itemId = changes?.itemFk || instance?.itemFk;
const oldQuantity = instance?.quantity ?? null;
const quantityAdded = newQuantity - oldQuantity;
const ticket = await models.Ticket.findById(
ticketId,
{
fields: ['id', 'clientFk', 'warehouseFk', 'addressFk', 'agencyModeFk', 'shipped', 'landed'],
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;
if (await models.ACL.checkAccessAcl(ctx, 'Sale', 'isInPreparing', '*')) return;
await models.Sale.rawSql(`CALL catalog_calcFromItem(?,?,?,?)`, [
ticket.landed,
ticket.addressFk,
ticket.agencyModeFk,
itemId
],
ctx.options);
const [itemInfo] = await models.Sale.rawSql(`SELECT available FROM tmp.ticketCalculateItem`, null, ctx.options);
if (!itemInfo?.available || itemInfo.available < quantityAdded)
throw new UserError(`This item is not available`);
if (await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*')) return;
if (newQuantity < item.minQuantity && newQuantity != itemInfo?.available)
throw new UserError('The amount cannot be less than the minimum');
if (ctx.isNewInstance || newQuantity <= oldQuantity) return;
const [saleGrouping] = await models.Sale.rawSql(`
SELECT t.price newPrice
FROM tmp.ticketComponentPrice t
ORDER BY (t.grouping <= ?) DESC, t.grouping ASC
LIMIT 1`,
[quantityAdded],
ctx.options);
await models.Sale.rawSql(`
DROP TEMPORARY TABLE IF EXISTS
tmp.ticketCalculateItem,
tmp.ticketComponentPrice,
tmp.ticketComponent,
tmp.ticketLot,
tmp.zoneGetShipped;
`, null, ctx.options);
if (!saleGrouping?.newPrice || saleGrouping.newPrice > instance.price)
throw new UserError('The price of the item changed');
});
}; };

View File

@ -0,0 +1,361 @@
/* eslint max-len: ["error", { "code": 150 }]*/
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale model ', () => {
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
function getActiveCtx(userId) {
return {
active: {
accessToken: {userId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
}
};
}
describe('quantity field ', () => {
it('should add quantity if the quantity is greater than it should be and is role advanced', async() => {
const saleId = 17;
const buyerId = 35;
const ctx = {
req: {
accessToken: {userId: buyerId},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const tx = await models.Sale.beginTransaction({});
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(buyerId));
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
SELECT 100 as available;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
try {
const options = {transaction: tx};
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
expect(isRoleAdvanced).toEqual(true);
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(30);
const newQuantity = originalLine.quantity + 1;
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
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;
try {
const options = {transaction: tx};
const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(originalLine.quantity).toEqual(20);
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an error if the quantity is negative and it is not a refund ticket', async() => {
const ctx = {
req: {
accessToken: {userId: 1},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(1));
const saleId = 17;
const newQuantity = -10;
const tx = await models.Sale.beginTransaction({});
let error;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('You can only add negative amounts in refund tickets'));
});
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;
try {
const options = {transaction: tx};
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options);
expect(modifiedLine.quantity).toEqual(newQuantity);
await tx.rollback();
} catch (e) {
await tx.rollback();
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);
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
SELECT 100 as available;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, 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.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY
SELECT ${newQuantity} as available;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
describe('newPrice', () => {
it('should increase quantity if you have enough available and the new price is the same as the previous one', 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 = 31;
try {
const options = {transaction: tx};
const item = await models.Item.findById(itemId, null, options);
await item.updateAttribute('minQuantity', minQuantity, options);
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available;
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 7.07 as price;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should increase quantity when the new price is lower than the previous one', 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 = 31;
try {
const options = {transaction: tx};
const item = await models.Item.findById(itemId, null, options);
await item.updateAttribute('minQuantity', minQuantity, options);
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available;
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 1 as price;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, options);
});
await models.Sale.updateQuantity(ctx, saleId, newQuantity, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw error when increase quantity and the new price is higher than the previous one', 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 = 31;
let error;
try {
const options = {transaction: tx};
const item = await models.Item.findById(itemId, null, options);
await item.updateAttribute('minQuantity', minQuantity, options);
spyOn(models.Sale, 'rawSql').and.callFake((sqlStatement, params, options) => {
if (sqlStatement.includes('catalog_calcFromItem')) {
sqlStatement = `
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketCalculateItem ENGINE = MEMORY SELECT ${newQuantity} as available;
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY SELECT 1 as grouping, 100000 as price;`;
params = null;
}
return models.Ticket.rawSql(sqlStatement, params, 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 price of the item changed'));
});
});
});
});

View File

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

View File

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

View File

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

View File

@ -311,7 +311,7 @@
clear-disabled="true" clear-disabled="true"
suffix="%"> suffix="%">
</vn-input-number> </vn-input-number>
<vn-vertical ng-if="$ctrl.usesMana && $ctrl.currentWorkerMana != 0"> <vn-vertical ng-if="$ctrl.usesMana">
<vn-radio <vn-radio
label="Promotion mana" label="Promotion mana"
val="mana" val="mana"

View File

@ -97,14 +97,6 @@ class Controller extends Section {
}); });
}); });
this.getUsesMana(); this.getUsesMana();
this.getCurrentWorkerMana();
}
getCurrentWorkerMana() {
this.$http.get(`WorkerManas/getCurrentWorkerMana`)
.then(res => {
this.currentWorkerMana = res.data;
});
} }
getUsesMana() { getUsesMana() {

View File

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

View File

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

View File

@ -132,12 +132,18 @@ module.exports = Self => {
s.nickname AS cargoSupplierNickname, s.nickname AS cargoSupplierNickname,
s.name AS supplierName, s.name AS supplierName,
CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg, 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 FROM travel t
LEFT JOIN supplier s ON s.id = t.cargoSupplierFk LEFT JOIN supplier s ON s.id = t.cargoSupplierFk
LEFT JOIN entry e ON e.travelFk = t.id LEFT JOIN entry e ON e.travelFk = t.id
LEFT JOIN buy b ON b.entryFk = e.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 item i ON i.id = b.itemFk
LEFT JOIN itemType it ON it.id = i.typeFk LEFT JOIN itemType it ON it.id = i.typeFk
JOIN warehouse w ON w.id = t.warehouseInFk JOIN warehouse w ON w.id = t.warehouseInFk
@ -169,11 +175,17 @@ module.exports = Self => {
e.evaNotes, e.evaNotes,
e.invoiceAmount, e.invoiceAmount,
CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) as loadedkg, 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 FROM tmp.travel tr
JOIN entry e ON e.travelFk = tr.id JOIN entry e ON e.travelFk = tr.id
JOIN buy b ON b.entryFk = e.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 item i ON i.id = b.itemFk
JOIN itemType it ON it.id = i.typeFk JOIN itemType it ON it.id = i.typeFk
JOIN supplier s ON s.id = e.supplierFk 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.buy b ON b.entryFk = e.id
LEFT JOIN vn.supplier s ON e.supplierFk = s.id LEFT JOIN vn.supplier s ON e.supplierFk = s.id
JOIN vn.item i ON i.id = b.itemFk 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 pcc ON pcc.id = 'cc'
JOIN vn.packaging ppallet ON ppallet.id = 'pallet 100' JOIN vn.packaging ppallet ON ppallet.id = 'pallet 100'
JOIN vn.packagingConfig pconfig 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;
};
};

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