Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5475-email_2fa
This commit is contained in:
commit
cdf092a989
|
@ -11,11 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
-
|
||||
|
||||
### Changed
|
||||
-
|
||||
- (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral
|
||||
|
||||
### Fixed
|
||||
-
|
||||
|
||||
|
||||
## [2314.01] - 2023-04-20
|
||||
|
||||
### Added
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const UserError = require('vn-loopback/util/user-error');
|
||||
const fs = require('fs-extra');
|
||||
const fs = require('fs/promises');
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('upload', {
|
||||
|
@ -36,7 +35,7 @@ module.exports = Self => {
|
|||
const fileOptions = {};
|
||||
const args = ctx.args;
|
||||
|
||||
let srcFile;
|
||||
let tempFilePath;
|
||||
try {
|
||||
const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection);
|
||||
if (!hasWriteRole)
|
||||
|
@ -53,15 +52,20 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
|
||||
srcFile = path.join(file.client.root, file.container, file.name);
|
||||
tempFilePath = path.join(file.client.root, file.container, file.name);
|
||||
|
||||
const fileName = `${uuid.v4()}.png`;
|
||||
await models.Image.registerImage(args.collection, srcFile, fileName, args.id);
|
||||
} catch (e) {
|
||||
if (fs.existsSync(srcFile))
|
||||
await fs.unlink(srcFile);
|
||||
const fileName = `${args.id}.png`;
|
||||
|
||||
throw e;
|
||||
await models.Image.resize({
|
||||
collectionName: args.collection,
|
||||
srcFile: tempFilePath,
|
||||
fileName: fileName,
|
||||
entityId: args.id
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch (error) { }
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -131,9 +131,6 @@
|
|||
"UserConfigView": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"UserLog": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"Warehouse": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -1,161 +1,110 @@
|
|||
const fs = require('fs-extra');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const readChunk = require('read-chunk');
|
||||
const imageType = require('image-type');
|
||||
const bmp = require('bmp-js');
|
||||
const gm = require('gm');
|
||||
|
||||
module.exports = Self => {
|
||||
require('../methods/image/download')(Self);
|
||||
require('../methods/image/upload')(Self);
|
||||
|
||||
// Function extracted from jimp package (utils)
|
||||
function scan(image, x, y, w, h, f) {
|
||||
// round input
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
w = Math.round(w);
|
||||
h = Math.round(h);
|
||||
|
||||
for (let _y = y; _y < y + h; _y++) {
|
||||
for (let _x = x; _x < x + w; _x++) {
|
||||
const idx = (image.bitmap.width * _y + _x) << 2;
|
||||
f.call(image, _x, _y, idx);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
// Function extracted from jimp package (type-bmp)
|
||||
function fromAGBR(bitmap) {
|
||||
return scan({bitmap}, 0, 0, bitmap.width, bitmap.height, function(
|
||||
x,
|
||||
y,
|
||||
index
|
||||
) {
|
||||
const alpha = this.bitmap.data[index + 0];
|
||||
const blue = this.bitmap.data[index + 1];
|
||||
const green = this.bitmap.data[index + 2];
|
||||
const red = this.bitmap.data[index + 3];
|
||||
|
||||
this.bitmap.data[index + 0] = red;
|
||||
this.bitmap.data[index + 1] = green;
|
||||
this.bitmap.data[index + 2] = blue;
|
||||
this.bitmap.data[index + 3] = bitmap.is_with_alpha ? alpha : 0xff;
|
||||
}).bitmap;
|
||||
}
|
||||
|
||||
Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
|
||||
Self.resize = async function({collectionName, srcFile, fileName, entityId}) {
|
||||
const models = Self.app.models;
|
||||
const tx = await Self.beginTransaction({});
|
||||
const myOptions = {transaction: tx};
|
||||
|
||||
try {
|
||||
const collection = await models.ImageCollection.findOne({
|
||||
const collection = await models.ImageCollection.findOne(
|
||||
{
|
||||
fields: [
|
||||
'id',
|
||||
'name',
|
||||
'maxWidth',
|
||||
'maxHeight',
|
||||
'model',
|
||||
'property'
|
||||
'property',
|
||||
],
|
||||
where: {name: collectionName},
|
||||
include: {
|
||||
relation: 'sizes',
|
||||
scope: {
|
||||
fields: ['width', 'height', 'crop']
|
||||
}
|
||||
}
|
||||
}, myOptions);
|
||||
fields: ['width', 'height', 'crop'],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = {
|
||||
// Insert image row
|
||||
await models.Image.upsertWithWhere(
|
||||
{
|
||||
name: fileName,
|
||||
collectionFk: collectionName
|
||||
};
|
||||
const newImage = await Self.upsertWithWhere(data, {
|
||||
},
|
||||
{
|
||||
name: fileName,
|
||||
collectionFk: collectionName,
|
||||
updated: Date.vnNow()
|
||||
}, myOptions);
|
||||
|
||||
// Resizes and saves the image
|
||||
const container = await models.ImageContainer.container(collectionName);
|
||||
const rootPath = container.client.root;
|
||||
const collectionDir = path.join(rootPath, collectionName);
|
||||
const dstDir = path.join(collectionDir, 'full');
|
||||
const dstFile = path.join(dstDir, fileName);
|
||||
|
||||
const buffer = readChunk.sync(srcFilePath, 0, 12);
|
||||
const type = imageType(buffer);
|
||||
|
||||
let sharpOptions;
|
||||
let imgSrc = srcFilePath;
|
||||
if (type.mime == 'image/bmp') {
|
||||
const bmpBuffer = fs.readFileSync(srcFilePath);
|
||||
const bmpData = fromAGBR(bmp.decode(bmpBuffer));
|
||||
imgSrc = bmpData.data;
|
||||
sharpOptions = {
|
||||
raw: {
|
||||
width: bmpData.width,
|
||||
height: bmpData.height,
|
||||
channels: 4
|
||||
},
|
||||
failOn: 'none'
|
||||
};
|
||||
updated: Date.vnNow() / 1000,
|
||||
}
|
||||
);
|
||||
|
||||
const resizeOpts = {
|
||||
withoutEnlargement: true,
|
||||
fit: 'inside'
|
||||
};
|
||||
// Update entity image file name
|
||||
const model = models[collection.model];
|
||||
if (!model) throw new Error('No matching model found');
|
||||
|
||||
await fs.mkdir(dstDir, {recursive: true});
|
||||
await sharp(imgSrc, sharpOptions)
|
||||
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
|
||||
.png()
|
||||
.toFile(dstFile);
|
||||
const entity = await model.findById(entityId);
|
||||
if (entity) {
|
||||
await entity.updateAttribute(
|
||||
collection.property,
|
||||
fileName
|
||||
);
|
||||
}
|
||||
|
||||
const sizes = collection.sizes();
|
||||
for (let size of sizes) {
|
||||
const dstDir = path.join(collectionDir, `${size.width}x${size.height}`);
|
||||
const dstFile = path.join(dstDir, fileName);
|
||||
const resizeOpts = {
|
||||
withoutEnlargement: true,
|
||||
fit: size.crop ? 'cover' : 'inside'
|
||||
};
|
||||
// Resize
|
||||
const container = await models.ImageContainer.container(
|
||||
collectionName
|
||||
);
|
||||
const rootPath = container.client.root;
|
||||
const collectionDir = path.join(rootPath, collectionName);
|
||||
|
||||
await fs.mkdir(dstDir, {recursive: true});
|
||||
await sharp(imgSrc, sharpOptions)
|
||||
.resize(size.width, size.height, resizeOpts)
|
||||
.png()
|
||||
.toFile(dstFile);
|
||||
}
|
||||
// To max size
|
||||
const {maxWidth, maxHeight} = collection;
|
||||
const fullSizePath = path.join(collectionDir, 'full');
|
||||
const toFullSizePath = `${fullSizePath}/${fileName}`;
|
||||
|
||||
const model = models[collection.model];
|
||||
await fs.mkdir(fullSizePath, {recursive: true});
|
||||
await new Promise((resolve, reject) => {
|
||||
gm(srcFile)
|
||||
.resize(maxWidth, maxHeight, '>')
|
||||
.setFormat('png')
|
||||
.quality(100)
|
||||
.write(toFullSizePath, function(err) {
|
||||
if (err) reject(err);
|
||||
if (!err) resolve();
|
||||
});
|
||||
});
|
||||
|
||||
if (!model)
|
||||
throw new Error('Matching model not found');
|
||||
// To collection sizes
|
||||
for (const size of collection.sizes()) {
|
||||
const {width, height} = size;
|
||||
|
||||
const item = await model.findById(entityId, null, myOptions);
|
||||
if (item) {
|
||||
await item.updateAttribute(
|
||||
collection.property,
|
||||
fileName,
|
||||
myOptions
|
||||
);
|
||||
}
|
||||
const sizePath = path.join(collectionDir, `${width}x${height}`);
|
||||
const toSizePath = `${sizePath}/${fileName}`;
|
||||
|
||||
if (fs.existsSync(srcFilePath))
|
||||
await fs.unlink(srcFilePath);
|
||||
await fs.mkdir(sizePath, {recursive: true});
|
||||
await new Promise((resolve, reject) => {
|
||||
const gmInstance = gm(srcFile);
|
||||
|
||||
await tx.commit();
|
||||
if (size.crop) {
|
||||
gmInstance
|
||||
.resize(width, height, '^')
|
||||
.gravity('Center')
|
||||
.crop(width, height);
|
||||
}
|
||||
|
||||
return newImage;
|
||||
} catch (e) {
|
||||
await tx.rollback();
|
||||
throw e;
|
||||
if (!size.crop) gmInstance.resize(width, height, '>');
|
||||
|
||||
gmInstance
|
||||
.setFormat('png')
|
||||
.quality(100)
|
||||
.write(toSizePath, function(err) {
|
||||
if (err) reject(err);
|
||||
if (!err) resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE `vn`.`ticketConfig` ADD daysForWarningClaim INT DEFAULT 2 NOT NULL COMMENT 'dias restantes hasta que salte el aviso de reclamación fuerade plazo';
|
|
@ -0,0 +1,74 @@
|
|||
DROP TABLE `vn`.`dmsRecover`;
|
||||
|
||||
ALTER TABLE `vn`.`delivery` DROP FOREIGN KEY delivery_FK;
|
||||
ALTER TABLE `vn`.`delivery` DROP COLUMN addressFk;
|
||||
ALTER TABLE `vn`.`delivery` ADD ticketFk INT NOT NULL;
|
||||
ALTER TABLE `vn`.`delivery` ADD CONSTRAINT delivery_ticketFk_FK FOREIGN KEY (`ticketFk`) REFERENCES `vn`.`ticket`(`id`);
|
||||
|
||||
DELETE FROM `salix`.`ACL` WHERE `property` = 'saveSign';
|
||||
INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalId`)
|
||||
VALUES
|
||||
('Ticket','saveSign','WRITE','ALLOW','employee');
|
||||
|
||||
DROP PROCEDURE IF EXISTS vn.route_getTickets;
|
||||
|
||||
DELIMITER $$
|
||||
$$
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`route_getTickets`(vRouteFk INT)
|
||||
BEGIN
|
||||
/**
|
||||
* Pasado un RouteFk devuelve la información
|
||||
* de sus tickets.
|
||||
*
|
||||
* @param vRouteFk
|
||||
*
|
||||
* @select Información de los tickets
|
||||
*/
|
||||
|
||||
SELECT
|
||||
t.id Id,
|
||||
t.clientFk Client,
|
||||
a.id Address,
|
||||
t.packages Packages,
|
||||
a.street AddressName,
|
||||
a.postalCode PostalCode,
|
||||
a.city City,
|
||||
sub2.itemPackingTypeFk PackingType,
|
||||
c.phone ClientPhone,
|
||||
c.mobile ClientMobile,
|
||||
a.phone AddressPhone,
|
||||
a.mobile AddressMobile,
|
||||
d.longitude Longitude,
|
||||
d.latitude Latitude,
|
||||
wm.mediaValue SalePersonPhone,
|
||||
tob.Note Note,
|
||||
t.isSigned Signed
|
||||
FROM ticket t
|
||||
JOIN client c ON t.clientFk = c.id
|
||||
JOIN address a ON t.addressFk = a.id
|
||||
LEFT JOIN delivery d ON t.id = d.ticketFk
|
||||
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
|
||||
LEFT JOIN
|
||||
(SELECT tob.description Note, t.id
|
||||
FROM ticketObservation tob
|
||||
JOIN ticket t ON tob.ticketFk = t.id
|
||||
JOIN observationType ot ON ot.id = tob.observationTypeFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
AND ot.code = 'delivery'
|
||||
)tob ON tob.id = t.id
|
||||
LEFT JOIN
|
||||
(SELECT sub.ticketFk,
|
||||
CONCAT('(', GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk ORDER BY sub.items DESC SEPARATOR ','), ') ') itemPackingTypeFk
|
||||
FROM (SELECT s.ticketFk , i.itemPackingTypeFk, COUNT(*) items
|
||||
FROM ticket t
|
||||
JOIN sale s ON s.ticketFk = t.id
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id,i.itemPackingTypeFk)sub
|
||||
GROUP BY sub.ticketFk
|
||||
) sub2 ON sub2.ticketFk = t.id
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id
|
||||
ORDER BY t.priority;
|
||||
END$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,67 @@
|
|||
DELETE FROM `salix`.`ACL` WHERE `property` = 'saveSign';
|
||||
INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalId`)
|
||||
VALUES
|
||||
('Ticket','saveSign','WRITE','ALLOW','employee');
|
||||
|
||||
DROP PROCEDURE IF EXISTS vn.route_getTickets;
|
||||
|
||||
DELIMITER $$
|
||||
$$
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`route_getTickets`(vRouteFk INT)
|
||||
BEGIN
|
||||
/**
|
||||
* Pasado un RouteFk devuelve la información
|
||||
* de sus tickets.
|
||||
*
|
||||
* @param vRouteFk
|
||||
*
|
||||
* @select Información de los tickets
|
||||
*/
|
||||
|
||||
SELECT
|
||||
t.id Id,
|
||||
t.clientFk Client,
|
||||
a.id Address,
|
||||
t.packages Packages,
|
||||
a.street AddressName,
|
||||
a.postalCode PostalCode,
|
||||
a.city City,
|
||||
sub2.itemPackingTypeFk PackingType,
|
||||
c.phone ClientPhone,
|
||||
c.mobile ClientMobile,
|
||||
a.phone AddressPhone,
|
||||
a.mobile AddressMobile,
|
||||
d.longitude Longitude,
|
||||
d.latitude Latitude,
|
||||
wm.mediaValue SalePersonPhone,
|
||||
tob.Note Note,
|
||||
t.isSigned Signed
|
||||
FROM ticket t
|
||||
JOIN client c ON t.clientFk = c.id
|
||||
JOIN address a ON t.addressFk = a.id
|
||||
LEFT JOIN delivery d ON t.id = d.ticketFk
|
||||
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
|
||||
LEFT JOIN
|
||||
(SELECT tob.description Note, t.id
|
||||
FROM ticketObservation tob
|
||||
JOIN ticket t ON tob.ticketFk = t.id
|
||||
JOIN observationType ot ON ot.id = tob.observationTypeFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
AND ot.code = 'delivery'
|
||||
)tob ON tob.id = t.id
|
||||
LEFT JOIN
|
||||
(SELECT sub.ticketFk,
|
||||
CONCAT('(', GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk ORDER BY sub.items DESC SEPARATOR ','), ') ') itemPackingTypeFk
|
||||
FROM (SELECT s.ticketFk , i.itemPackingTypeFk, COUNT(*) items
|
||||
FROM ticket t
|
||||
JOIN sale s ON s.ticketFk = t.id
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id,i.itemPackingTypeFk)sub
|
||||
GROUP BY sub.ticketFk
|
||||
) sub2 ON sub2.ticketFk = t.id
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id
|
||||
ORDER BY t.priority;
|
||||
END$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,83 @@
|
|||
CREATE TABLE `vn`.`dmsRecover` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`ticketFk` int(11) DEFAULT NULL,
|
||||
`sign` text DEFAULT NULL,
|
||||
`created` timestamp NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `ticketFk_idx` (`ticketFk`),
|
||||
CONSTRAINT `ticketFk` FOREIGN KEY (`ticketFk`) REFERENCES `ticket` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=31917 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
|
||||
|
||||
ALTER TABLE `vn`.`delivery` ADD addressFk INT;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `vn`.`route_getTickets`;
|
||||
|
||||
DELIMITER $$
|
||||
$$
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`route_getTickets`(vRouteFk INT)
|
||||
BEGIN
|
||||
/**
|
||||
* Pasado un RouteFk devuelve la información
|
||||
* de sus tickets.
|
||||
*
|
||||
* @param vRouteFk
|
||||
* @select Información de los tickets
|
||||
*/
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT t.id Id,
|
||||
t.clientFk Client,
|
||||
a.id Address,
|
||||
a.nickname ClientName,
|
||||
t.packages Packages,
|
||||
a.street AddressName,
|
||||
a.postalCode PostalCode,
|
||||
a.city City,
|
||||
sub2.itemPackingTypeFk PackingType,
|
||||
c.phone ClientPhone,
|
||||
c.mobile ClientMobile,
|
||||
a.phone AddressPhone,
|
||||
a.mobile AddressMobile,
|
||||
d.longitude Longitude,
|
||||
d.latitude Latitude,
|
||||
wm.mediaValue SalePersonPhone,
|
||||
tob.description Note,
|
||||
t.isSigned Signed,
|
||||
t.priority
|
||||
FROM ticket t
|
||||
JOIN client c ON t.clientFk = c.id
|
||||
JOIN address a ON t.addressFk = a.id
|
||||
LEFT JOIN delivery d ON d.addressFk = a.id
|
||||
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
|
||||
LEFT JOIN(
|
||||
SELECT tob.description, t.id
|
||||
FROM ticketObservation tob
|
||||
JOIN ticket t ON tob.ticketFk = t.id
|
||||
JOIN observationType ot ON ot.id = tob.observationTypeFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
AND ot.code = 'delivery'
|
||||
)tob ON tob.id = t.id
|
||||
LEFT JOIN(
|
||||
SELECT sub.ticketFk,
|
||||
CONCAT('(',
|
||||
GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk
|
||||
ORDER BY sub.items DESC SEPARATOR ','),
|
||||
') ') itemPackingTypeFk
|
||||
FROM (
|
||||
SELECT s.ticketFk, i.itemPackingTypeFk, COUNT(*) items
|
||||
FROM ticket t
|
||||
JOIN sale s ON s.ticketFk = t.id
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id, i.itemPackingTypeFk
|
||||
)sub
|
||||
GROUP BY sub.ticketFk
|
||||
)sub2 ON sub2.ticketFk = t.id
|
||||
WHERE t.routeFk = vRouteFk
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 10000000000000000000
|
||||
)sub3
|
||||
GROUP BY sub3.id
|
||||
ORDER BY sub3.priority;
|
||||
END$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,70 @@
|
|||
DROP TABLE IF EXISTS `vn`.`dmsRecover`;
|
||||
|
||||
ALTER TABLE `vn`.`delivery` DROP COLUMN addressFk;
|
||||
ALTER TABLE `vn`.`delivery` DROP CONSTRAINT delivery_ticketFk_FK;
|
||||
ALTER TABLE `vn`.`delivery` DROP COLUMN ticketFk;
|
||||
ALTER TABLE `vn`.`delivery` ADD ticketFk INT DEFAULT NULL;
|
||||
ALTER TABLE `vn`.`delivery` ADD CONSTRAINT delivery_ticketFk_FK FOREIGN KEY (`ticketFk`) REFERENCES `vn`.`ticket`(`id`);
|
||||
|
||||
DROP PROCEDURE IF EXISTS vn.route_getTickets;
|
||||
|
||||
DELIMITER $$
|
||||
$$
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`route_getTickets`(vRouteFk INT)
|
||||
BEGIN
|
||||
/**
|
||||
* Pasado un RouteFk devuelve la información
|
||||
* de sus tickets.
|
||||
*
|
||||
* @param vRouteFk
|
||||
*
|
||||
* @select Información de los tickets
|
||||
*/
|
||||
|
||||
SELECT
|
||||
t.id Id,
|
||||
t.clientFk Client,
|
||||
a.id Address,
|
||||
t.packages Packages,
|
||||
a.street AddressName,
|
||||
a.postalCode PostalCode,
|
||||
a.city City,
|
||||
sub2.itemPackingTypeFk PackingType,
|
||||
c.phone ClientPhone,
|
||||
c.mobile ClientMobile,
|
||||
a.phone AddressPhone,
|
||||
a.mobile AddressMobile,
|
||||
d.longitude Longitude,
|
||||
d.latitude Latitude,
|
||||
wm.mediaValue SalePersonPhone,
|
||||
tob.Note Note,
|
||||
t.isSigned Signed
|
||||
FROM ticket t
|
||||
JOIN client c ON t.clientFk = c.id
|
||||
JOIN address a ON t.addressFk = a.id
|
||||
LEFT JOIN delivery d ON t.id = d.ticketFk
|
||||
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
|
||||
LEFT JOIN
|
||||
(SELECT tob.description Note, t.id
|
||||
FROM ticketObservation tob
|
||||
JOIN ticket t ON tob.ticketFk = t.id
|
||||
JOIN observationType ot ON ot.id = tob.observationTypeFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
AND ot.code = 'delivery'
|
||||
)tob ON tob.id = t.id
|
||||
LEFT JOIN
|
||||
(SELECT sub.ticketFk,
|
||||
CONCAT('(', GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk ORDER BY sub.items DESC SEPARATOR ','), ') ') itemPackingTypeFk
|
||||
FROM (SELECT s.ticketFk , i.itemPackingTypeFk, COUNT(*) items
|
||||
FROM ticket t
|
||||
JOIN sale s ON s.ticketFk = t.id
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id,i.itemPackingTypeFk)sub
|
||||
GROUP BY sub.ticketFk
|
||||
) sub2 ON sub2.ticketFk = t.id
|
||||
WHERE t.routeFk = vRouteFk
|
||||
GROUP BY t.id
|
||||
ORDER BY t.priority;
|
||||
END$$
|
||||
DELIMITER ;
|
|
@ -0,0 +1,3 @@
|
|||
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||
VALUES
|
||||
('Defaulter', 'observationEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee');
|
|
@ -0,0 +1,4 @@
|
|||
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||
VALUES
|
||||
('UserLog', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
|
||||
('RoleLog', '*', 'READ', 'ALLOW', 'ROLE', 'employee');
|
|
@ -414,7 +414,7 @@ export default {
|
|||
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
|
||||
},
|
||||
itemFixedPrice: {
|
||||
add: 'vn-fixed-price vn-icon-button[icon="add_circle"]',
|
||||
add: 'vn-fixed-price vn-icon-button[vn-tooltip="Add fixed price"]',
|
||||
firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]',
|
||||
fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)',
|
||||
fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]',
|
||||
|
@ -427,7 +427,18 @@ export default {
|
|||
fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]',
|
||||
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]',
|
||||
orderColumnId: 'vn-fixed-price th[field="itemFk"]',
|
||||
removeWarehouseFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(1) > vn-icon > i'
|
||||
removeWarehouseFilter: 'vn-searchbar > form > vn-textfield > div.container > div.prepend > prepend > div > span:nth-child(1) > vn-icon > i',
|
||||
generalSearchFilter: 'vn-fixed-price-search-panel vn-textfield[ng-model="$ctrl.filter.search"]',
|
||||
reignFilter: 'vn-fixed-price-search-panel vn-horizontal.item-category vn-one',
|
||||
typeFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.typeFk"]',
|
||||
buyerFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.buyerFk"]',
|
||||
warehouseFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="$ctrl.filter.warehouseFk"]',
|
||||
mineFilter: 'vn-fixed-price-search-panel vn-check[ng-model="$ctrl.filter.mine"]',
|
||||
hasMinPriceFilter: 'vn-fixed-price-search-panel vn-check[ng-model="$ctrl.filter.hasMinPrice"]',
|
||||
addTag: 'vn-fixed-price-search-panel vn-icon-button[icon="add_circle"]',
|
||||
tagFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="itemTag.tagFk"]',
|
||||
tagValueFilter: 'vn-fixed-price-search-panel vn-autocomplete[ng-model="itemTag.value"]',
|
||||
chip: 'vn-fixed-price-search-panel vn-chip > vn-icon'
|
||||
},
|
||||
itemCreateView: {
|
||||
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
|
||||
|
@ -989,9 +1000,9 @@ export default {
|
|||
saveButton: 'vn-worker-basic-data button[type=submit]'
|
||||
},
|
||||
workerNotes: {
|
||||
addNoteFloatButton: 'vn-float-button',
|
||||
note: 'vn-textarea[ng-model="$ctrl.note.text"]',
|
||||
saveButton: 'button[type=submit]',
|
||||
addNoteFloatButton: 'vn-worker-note vn-icon[icon="add"]',
|
||||
note: 'vn-note-worker-create vn-textarea[ng-model="$ctrl.note.text"]',
|
||||
saveButton: 'vn-note-worker-create button[type=submit]',
|
||||
firstNoteText: 'vn-worker-note .text'
|
||||
},
|
||||
workerPbx: {
|
||||
|
|
|
@ -28,22 +28,4 @@ describe('Client log path', () => {
|
|||
it('should navigate to the log section', async() => {
|
||||
await page.accessToSection('client.card.log');
|
||||
});
|
||||
|
||||
it('should check the previous value of the last logged change', async() => {
|
||||
let lastModificationPreviousValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText');
|
||||
|
||||
expect(lastModificationPreviousValue).toContain('DavidCharlesHaller');
|
||||
});
|
||||
|
||||
it('should check the current value of the last logged change', async() => {
|
||||
let lastModificationPreviousValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText');
|
||||
|
||||
let lastModificationCurrentValue = await page.
|
||||
waitToGetProperty(selectors.clientLog.lastModificationCurrentValue, 'innerText');
|
||||
|
||||
expect(lastModificationPreviousValue).toEqual('DavidCharlesHaller');
|
||||
expect(lastModificationCurrentValue).toEqual('this is a test');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ describe('Client defaulter path', () => {
|
|||
expect(message.text).toContain(`The message can't be empty`);
|
||||
});
|
||||
|
||||
it('shoul checked all defaulters', async() => {
|
||||
it('should checked all defaulters', async() => {
|
||||
await page.loginAndModule('insurance', 'client');
|
||||
await page.accessToSection('client.defaulter');
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ describe('Worker Add notes path', () => {
|
|||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('employee', 'worker');
|
||||
await page.loginAndModule('hr', 'worker');
|
||||
await page.accessToSearchResult('Bruce Banner');
|
||||
await page.accessToSection('worker.card.note.index');
|
||||
});
|
||||
|
|
|
@ -42,23 +42,4 @@ describe('Item log path', () => {
|
|||
await page.waitForSelector(selectors.itemsIndex.createItemButton);
|
||||
await page.waitForState('item.index');
|
||||
});
|
||||
|
||||
it(`should search for the created item and navigate to it's log section`, async() => {
|
||||
await page.accessToSearchResult('Knowledge artifact');
|
||||
await page.accessToSection('item.card.log');
|
||||
});
|
||||
|
||||
it(`should confirm the log is showing 4 entries`, async() => {
|
||||
await page.waitForSelector(selectors.itemLog.anyLineCreated);
|
||||
const anyLineCreatedCount = await page.countElement(selectors.itemLog.anyLineCreated);
|
||||
|
||||
expect(anyLineCreatedCount).toEqual(4);
|
||||
});
|
||||
|
||||
xit(`should confirm the log is showing the intrastat for the created item`, async() => {
|
||||
const fifthLineCreatedProperty = await page
|
||||
.waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText');
|
||||
|
||||
expect(fifthLineCreatedProperty).toEqual('05080000');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,20 +4,69 @@ import getBrowser from '../../helpers/puppeteer';
|
|||
describe('Item fixed prices path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
let httpRequest;
|
||||
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('buyer', 'item');
|
||||
await page.accessToSection('item.fixedPrice');
|
||||
page.on('request', req => {
|
||||
if (req.url().includes(`FixedPrices/filter`))
|
||||
httpRequest = req.url();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should filter using all the fields', async() => {
|
||||
await page.write(selectors.itemFixedPrice.generalSearchFilter, 'item');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
expect(httpRequest).toContain('search=item');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.click(selectors.itemFixedPrice.reignFilter);
|
||||
|
||||
expect(httpRequest).toContain('categoryFk');
|
||||
|
||||
await page.autocompleteSearch(selectors.itemFixedPrice.typeFilter, 'Alstroemeria');
|
||||
|
||||
expect(httpRequest).toContain('typeFk');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.autocompleteSearch(selectors.itemFixedPrice.buyerFilter, 'buyerNick');
|
||||
|
||||
expect(httpRequest).toContain('buyerFk');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.autocompleteSearch(selectors.itemFixedPrice.warehouseFilter, 'Algemesi');
|
||||
|
||||
expect(httpRequest).toContain('warehouseFk');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.click(selectors.itemFixedPrice.mineFilter);
|
||||
|
||||
expect(httpRequest).toContain('mine=true');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.click(selectors.itemFixedPrice.hasMinPriceFilter);
|
||||
|
||||
expect(httpRequest).toContain('hasMinPrice=true');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
await page.click(selectors.itemFixedPrice.addTag);
|
||||
await page.autocompleteSearch(selectors.itemFixedPrice.tagFilter, 'Color');
|
||||
await page.autocompleteSearch(selectors.itemFixedPrice.tagValueFilter, 'Brown');
|
||||
|
||||
expect(httpRequest).toContain('tags');
|
||||
|
||||
await page.click(selectors.itemFixedPrice.chip);
|
||||
});
|
||||
|
||||
it('should click on the add new fixed price button', async() => {
|
||||
await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter);
|
||||
await page.waitForSpinnerLoad();
|
||||
await page.waitToClick(selectors.itemFixedPrice.add);
|
||||
await page.waitForSelector(selectors.itemFixedPrice.fourthFixedPrice);
|
||||
});
|
||||
|
@ -36,10 +85,7 @@ describe('Item fixed prices path', () => {
|
|||
});
|
||||
|
||||
it('should reload the section and check the created price has the expected ID', async() => {
|
||||
await page.accessToSection('item.index');
|
||||
await page.accessToSection('item.fixedPrice');
|
||||
await page.waitToClick(selectors.itemFixedPrice.removeWarehouseFilter);
|
||||
await page.waitForSpinnerLoad();
|
||||
await page.goto(`http://localhost:5000/#!/item/fixed-price`);
|
||||
|
||||
const result = await page.waitToGetProperty(selectors.itemFixedPrice.fourthItemID, 'value');
|
||||
|
||||
|
|
|
@ -249,6 +249,7 @@ describe('Ticket Edit sale path', () => {
|
|||
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
|
||||
await page.waitToClick(selectors.ticketSales.moreMenu);
|
||||
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);
|
||||
await page.waitToClick(selectors.globalItems.acceptButton);
|
||||
await page.waitForState('claim.card.basicData');
|
||||
});
|
||||
|
||||
|
|
|
@ -29,20 +29,4 @@ describe('Ticket expeditions and log path', () => {
|
|||
|
||||
expect(result).toEqual(3);
|
||||
});
|
||||
|
||||
it(`should confirm the expedition deleted is shown now in the ticket log`, async() => {
|
||||
await page.accessToSection('ticket.card.log');
|
||||
const user = await page
|
||||
.waitToGetProperty(selectors.ticketLog.user, 'innerText');
|
||||
|
||||
const action = await page
|
||||
.waitToGetProperty(selectors.ticketLog.action, 'innerText');
|
||||
|
||||
const id = await page
|
||||
.waitToGetProperty(selectors.ticketLog.id, 'innerText');
|
||||
|
||||
expect(user).toContain('production');
|
||||
expect(action).toContain('Deletes');
|
||||
expect(id).toEqual('2');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,30 +31,4 @@ describe('Ticket log path', () => {
|
|||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it('should navigate to the log section', async() => {
|
||||
await page.accessToSection('ticket.card.log');
|
||||
});
|
||||
|
||||
it('should set the viewport width to 1920 to see the table full width', async() => {
|
||||
await page.setViewport({
|
||||
width: 1920,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const result = await page.waitToGetProperty(selectors.ticketLog.firstTD, 'innerText');
|
||||
|
||||
expect(result.length).not.toBeGreaterThan('20');
|
||||
});
|
||||
|
||||
it('should set the viewport width to 800 to see the table shrink and move data to the 1st column', async() => {
|
||||
await page.setViewport({
|
||||
width: 800,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const result = await page.waitToGetProperty(selectors.ticketLog.firstTD, 'innerText');
|
||||
|
||||
expect(result.length).toBeGreaterThan('15');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,14 +29,4 @@ describe('Zone descriptor path', () => {
|
|||
|
||||
expect(count).toEqual(0);
|
||||
});
|
||||
|
||||
it('should check the ticket whom lost the zone and see evidence on the logs', async() => {
|
||||
await page.waitToClick(selectors.globalItems.homeButton);
|
||||
await page.selectModule('ticket');
|
||||
await page.accessToSearchResult('20');
|
||||
await page.accessToSection('ticket.card.log');
|
||||
const lastChanges = await page.waitToGetProperty(selectors.ticketLog.changes, 'innerText');
|
||||
|
||||
expect(lastChanges).toContain('1');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,14 +64,4 @@ describe('Supplier basic data path', () => {
|
|||
|
||||
expect(result).toEqual('Some notes');
|
||||
});
|
||||
|
||||
it('should navigate to the log section', async() => {
|
||||
await page.accessToSection('supplier.card.log');
|
||||
});
|
||||
|
||||
it('should check the changes have been recorded', async() => {
|
||||
const result = await page.waitToGetProperty('vn-tr table tr:nth-child(3) td.after', 'innerText');
|
||||
|
||||
expect(result).toEqual('Some notes');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -162,14 +162,8 @@ export default class UploadPhoto extends Component {
|
|||
if (!this.newPhoto.files)
|
||||
throw new Error(`Select an image`);
|
||||
|
||||
const viewportType = this.viewportSelection;
|
||||
const output = viewportType.output;
|
||||
const options = {
|
||||
type: 'blob',
|
||||
size: {
|
||||
width: output.width,
|
||||
height: output.height
|
||||
}
|
||||
};
|
||||
return this.editor.result(options)
|
||||
.then(blob => this.newPhoto.blob = blob)
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('Model crud()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
let insertId;
|
||||
const barcodeModel = app.models.ItemBarcode;
|
||||
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('Model rewriteDbError()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should extend rewriteDbError properties to any model passed', () => {
|
||||
const exampleModel = models.ItemTag;
|
||||
|
||||
|
|
|
@ -154,5 +154,6 @@
|
|||
"Valid priorities: 1,2,3": "Valid priorities: 1,2,3",
|
||||
"Warehouse inventory not set": "Almacén inventario no está establecido",
|
||||
"Component cost not set": "Componente coste no está estabecido",
|
||||
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2"
|
||||
"Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2": "Tickets with associated refunds can't be deleted. This ticket is associated with refund Nº 2",
|
||||
"Description cannot be blank": "Description cannot be blank"
|
||||
}
|
|
@ -1,41 +1,9 @@
|
|||
const mysql = require('mysql');
|
||||
const MySQL = require('loopback-connector-mysql').MySQL;
|
||||
const EnumFactory = require('loopback-connector-mysql').EnumFactory;
|
||||
const { Transaction, SQLConnector, ParameterizedSQL } = require('loopback-connector');
|
||||
const {Transaction, SQLConnector, ParameterizedSQL} = require('loopback-connector');
|
||||
const fs = require('fs');
|
||||
|
||||
const limitSet = new Set([
|
||||
'save',
|
||||
'updateOrCreate',
|
||||
'replaceOrCreate',
|
||||
'replaceById',
|
||||
'update'
|
||||
]);
|
||||
|
||||
const opOpts = {
|
||||
update: [
|
||||
'update',
|
||||
'replaceById',
|
||||
// |insert
|
||||
'save',
|
||||
'updateOrCreate',
|
||||
'replaceOrCreate'
|
||||
],
|
||||
delete: [
|
||||
'destroy',
|
||||
'destroyAll'
|
||||
],
|
||||
insert: [
|
||||
'create'
|
||||
]
|
||||
};
|
||||
|
||||
const opMap = new Map();
|
||||
for (const op in opOpts) {
|
||||
for (const met of opOpts[op])
|
||||
opMap.set(met, op);
|
||||
}
|
||||
|
||||
class VnMySQL extends MySQL {
|
||||
/**
|
||||
* Promisified version of execute().
|
||||
|
@ -253,49 +221,49 @@ class VnMySQL extends MySQL {
|
|||
}
|
||||
|
||||
create(model, data, opts, cb) {
|
||||
const ctx = { data };
|
||||
const ctx = {data};
|
||||
this.invokeMethod('create',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
createAll(model, data, opts, cb) {
|
||||
const ctx = { data };
|
||||
const ctx = {data};
|
||||
this.invokeMethod('createAll',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
save(model, data, opts, cb) {
|
||||
const ctx = { data };
|
||||
const ctx = {data};
|
||||
this.invokeMethod('save',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
updateOrCreate(model, data, opts, cb) {
|
||||
const ctx = { data };
|
||||
const ctx = {data};
|
||||
this.invokeMethod('updateOrCreate',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
replaceOrCreate(model, data, opts, cb) {
|
||||
const ctx = { data };
|
||||
const ctx = {data};
|
||||
this.invokeMethod('replaceOrCreate',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
destroyAll(model, where, opts, cb) {
|
||||
const ctx = { where };
|
||||
const ctx = {where};
|
||||
this.invokeMethod('destroyAll',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
update(model, where, data, opts, cb) {
|
||||
const ctx = { where, data };
|
||||
const ctx = {where, data};
|
||||
this.invokeMethod('update',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
||||
replaceById(model, id, data, opts, cb) {
|
||||
const ctx = { id, data };
|
||||
const ctx = {id, data};
|
||||
this.invokeMethod('replaceById',
|
||||
arguments, model, ctx, opts, cb);
|
||||
}
|
||||
|
@ -316,45 +284,17 @@ class VnMySQL extends MySQL {
|
|||
|
||||
async invokeMethodP(method, args, model, ctx, opts) {
|
||||
const Model = this.getModelDefinition(model).model;
|
||||
const settings = Model.definition.settings;
|
||||
let tx;
|
||||
if (!opts.transaction) {
|
||||
tx = await Transaction.begin(this, {});
|
||||
opts = Object.assign({ transaction: tx, httpCtx: opts.httpCtx }, opts);
|
||||
opts = Object.assign({transaction: tx, httpCtx: opts.httpCtx}, opts);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch old values (update|delete) or login
|
||||
let where, id, data, idName, limit, op, oldInstances, newInstances;
|
||||
const hasGrabUser = settings.log && settings.log.grabUser;
|
||||
if (hasGrabUser) {
|
||||
const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
|
||||
const user = await Model.app.models.Account.findById(userId, { fields: ['name'] }, opts);
|
||||
const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
|
||||
if (userId) {
|
||||
const user = await Model.app.models.Account.findById(userId, {fields: ['name']}, opts);
|
||||
await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts);
|
||||
} else {
|
||||
where = ctx.where;
|
||||
id = ctx.id;
|
||||
data = ctx.data;
|
||||
idName = this.idName(model);
|
||||
|
||||
limit = limitSet.has(method);
|
||||
|
||||
op = opMap.get(method);
|
||||
|
||||
if (!where) {
|
||||
if (id) where = { [idName]: id };
|
||||
else where = { [idName]: data[idName] };
|
||||
}
|
||||
|
||||
// Fetch old values
|
||||
switch (op) {
|
||||
case 'update':
|
||||
case 'delete':
|
||||
// Single entity operation
|
||||
const stmt = this.buildSelectStmt(op, data, idName, model, where, limit);
|
||||
stmt.merge(`FOR UPDATE`);
|
||||
oldInstances = await this.executeStmt(stmt, opts);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await new Promise((resolve, reject) => {
|
||||
|
@ -366,38 +306,7 @@ class VnMySQL extends MySQL {
|
|||
super[method].apply(this, fnArgs);
|
||||
});
|
||||
|
||||
if (hasGrabUser)
|
||||
await this.executeP(`CALL account.myUser_logout()`, null, opts);
|
||||
else {
|
||||
// Fetch new values
|
||||
const ids = [];
|
||||
|
||||
switch (op) {
|
||||
case 'insert':
|
||||
case 'update': {
|
||||
switch (method) {
|
||||
case 'createAll':
|
||||
for (const row of res[0])
|
||||
ids.push(row[idName]);
|
||||
break;
|
||||
case 'create':
|
||||
ids.push(res[0]);
|
||||
break;
|
||||
case 'update':
|
||||
if (data[idName] != null)
|
||||
ids.push(data[idName]);
|
||||
break;
|
||||
}
|
||||
|
||||
const newWhere = ids.length ? {[idName]: {inq: ids}} : where;
|
||||
|
||||
const stmt = this.buildSelectStmt(op, data, idName, model, newWhere, limit);
|
||||
newInstances = await this.executeStmt(stmt, opts);
|
||||
}
|
||||
}
|
||||
|
||||
await this.createLogRecord(oldInstances, newInstances, model, opts);
|
||||
}
|
||||
if (userId) await this.executeP(`CALL account.myUser_logout()`, null, opts);
|
||||
if (tx) await tx.commit();
|
||||
return res;
|
||||
} catch (err) {
|
||||
|
@ -405,125 +314,6 @@ class VnMySQL extends MySQL {
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
buildSelectStmt(op, data, idName, model, where, limit) {
|
||||
const Model = this.getModelDefinition(model).model;
|
||||
const properties = Object.keys(Model.definition.properties);
|
||||
|
||||
const fields = data ? Object.keys(data) : [];
|
||||
if (op == 'delete')
|
||||
properties.forEach(property => fields.push(property));
|
||||
else {
|
||||
const log = Model.definition.settings.log;
|
||||
fields.push(idName);
|
||||
if (log.relation) fields.push(Model.relations[log.relation].keyFrom);
|
||||
if (log.showField) fields.push(log.showField);
|
||||
else {
|
||||
const showFieldNames = ['name', 'description', 'code', 'nickname'];
|
||||
for (const field of showFieldNames) {
|
||||
if (properties.includes(field)) {
|
||||
log.showField = field;
|
||||
fields.push(field);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stmt = new ParameterizedSQL(
|
||||
'SELECT ' +
|
||||
this.buildColumnNames(model, { fields }) +
|
||||
' FROM ' +
|
||||
this.tableEscaped(model)
|
||||
);
|
||||
stmt.merge(this.buildWhere(model, where));
|
||||
if (limit) stmt.merge(`LIMIT 1`);
|
||||
|
||||
return stmt;
|
||||
}
|
||||
|
||||
async createLogRecord(oldInstances, newInstances, model, opts) {
|
||||
function setActionType() {
|
||||
if (oldInstances && newInstances)
|
||||
return 'update';
|
||||
else if (!oldInstances && newInstances)
|
||||
return 'insert';
|
||||
return 'delete';
|
||||
}
|
||||
|
||||
const action = setActionType();
|
||||
if (!newInstances && action != 'delete') return;
|
||||
|
||||
const Model = this.getModelDefinition(model).model;
|
||||
const models = Model.app.models;
|
||||
const definition = Model.definition;
|
||||
const log = definition.settings.log;
|
||||
|
||||
const primaryKey = this.idName(model);
|
||||
const originRelation = log.relation;
|
||||
const originFkField = originRelation
|
||||
? Model.relations[originRelation].keyFrom
|
||||
: primaryKey;
|
||||
|
||||
// Prevent adding logs when deleting a principal entity (Client, Zone...)
|
||||
if (action == 'delete' && !originRelation) return;
|
||||
|
||||
function map(instances) {
|
||||
const map = new Map();
|
||||
if (!instances) return;
|
||||
for (const instance of instances)
|
||||
map.set(instance[primaryKey], instance);
|
||||
return map;
|
||||
}
|
||||
|
||||
const changedModel = definition.name;
|
||||
const userFk = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
|
||||
const oldMap = map(oldInstances);
|
||||
const newMap = map(newInstances);
|
||||
const ids = (oldMap || newMap).keys();
|
||||
|
||||
const logEntries = [];
|
||||
|
||||
function insertValuesLogEntry(logEntry, instance) {
|
||||
logEntry.originFk = instance[originFkField];
|
||||
logEntry.changedModelId = instance[primaryKey];
|
||||
if (log.showField) logEntry.changedModelValue = instance[log.showField];
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
const oldI = oldMap && oldMap.get(id);
|
||||
const newI = newMap && newMap.get(id);
|
||||
|
||||
const logEntry = {
|
||||
action,
|
||||
userFk,
|
||||
changedModel,
|
||||
};
|
||||
|
||||
if (newI) {
|
||||
insertValuesLogEntry(logEntry, newI);
|
||||
// Delete unchanged properties
|
||||
if (oldI) {
|
||||
Object.keys(oldI).forEach(prop => {
|
||||
const hasChanges = oldI[prop] instanceof Date ?
|
||||
oldI[prop]?.getTime() != newI[prop]?.getTime() :
|
||||
oldI[prop] != newI[prop];
|
||||
|
||||
if (!hasChanges) {
|
||||
delete oldI[prop];
|
||||
delete newI[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
} else
|
||||
insertValuesLogEntry(logEntry, oldI);
|
||||
|
||||
logEntry.oldInstance = oldI;
|
||||
logEntry.newInstance = newI;
|
||||
logEntries.push(logEntry);
|
||||
}
|
||||
await models[log.model].create(logEntries, opts);
|
||||
}
|
||||
}
|
||||
|
||||
exports.VnMySQL = VnMySQL;
|
||||
|
@ -544,7 +334,7 @@ exports.initialize = function initialize(dataSource, callback) {
|
|||
|
||||
if (callback) {
|
||||
if (dataSource.settings.lazyConnect) {
|
||||
process.nextTick(function () {
|
||||
process.nextTick(function() {
|
||||
callback();
|
||||
});
|
||||
} else
|
||||
|
@ -552,13 +342,13 @@ exports.initialize = function initialize(dataSource, callback) {
|
|||
}
|
||||
};
|
||||
|
||||
MySQL.prototype.connect = function (callback) {
|
||||
MySQL.prototype.connect = function(callback) {
|
||||
const self = this;
|
||||
const options = generateOptions(this.settings);
|
||||
|
||||
if (this.client) {
|
||||
if (callback) {
|
||||
process.nextTick(function () {
|
||||
process.nextTick(function() {
|
||||
callback(null, self.client);
|
||||
});
|
||||
}
|
||||
|
@ -567,7 +357,7 @@ MySQL.prototype.connect = function (callback) {
|
|||
|
||||
function connectionHandler(options, callback) {
|
||||
const client = mysql.createPool(options);
|
||||
client.getConnection(function (err, connection) {
|
||||
client.getConnection(function(err, connection) {
|
||||
const conn = connection;
|
||||
if (!err) {
|
||||
if (self.debug)
|
||||
|
@ -647,30 +437,27 @@ function generateOptions(settings) {
|
|||
return options;
|
||||
}
|
||||
|
||||
|
||||
SQLConnector.prototype.all = function find(model, filter, options, cb) {
|
||||
const self = this;
|
||||
// Order by id if no order is specified
|
||||
filter = filter || {};
|
||||
const stmt = this.buildSelect(model, filter, options);
|
||||
this.execute(stmt.sql, stmt.params, options, function (err, data) {
|
||||
if (err) {
|
||||
this.execute(stmt.sql, stmt.params, options, function(err, data) {
|
||||
if (err)
|
||||
return cb(err, []);
|
||||
}
|
||||
|
||||
try {
|
||||
const objs = data.map(function (obj) {
|
||||
const objs = data.map(function(obj) {
|
||||
return self.fromRow(model, obj);
|
||||
});
|
||||
if (filter && filter.include) {
|
||||
self.getModelDefinition(model).model.include(
|
||||
objs, filter.include, options, cb,
|
||||
);
|
||||
} else {
|
||||
} else
|
||||
cb(null, objs);
|
||||
}
|
||||
} catch (error) {
|
||||
cb(error, [])
|
||||
cb(error, []);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
"RoleConfig": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"RoleLog": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"RoleInherit": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
@ -41,6 +44,9 @@
|
|||
"UserAccount": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"UserLog": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"UserPassword": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"name": "RoleLog",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "account.roleLog"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"id": true,
|
||||
"type": "number",
|
||||
"forceId": false
|
||||
},
|
||||
"originFk": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
"userFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"changedModel": {
|
||||
"type": "string"
|
||||
},
|
||||
"oldInstance": {
|
||||
"type": "object"
|
||||
},
|
||||
"newInstance": {
|
||||
"type": "object"
|
||||
},
|
||||
"creationDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"changedModelId": {
|
||||
"type": "number"
|
||||
},
|
||||
"changedModelValue": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "Account",
|
||||
"foreignKey": "userFk"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"order": ["creationDate DESC", "id DESC"]
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "userLog"
|
||||
"table": "account.userLog"
|
||||
}
|
||||
},
|
||||
"properties": {
|
|
@ -19,3 +19,5 @@ import './ldap';
|
|||
import './samba';
|
||||
import './accounts';
|
||||
import './privileges';
|
||||
import './user-log';
|
||||
import './role-log';
|
||||
|
|
|
@ -9,3 +9,4 @@ Mail aliases: Alias de correo
|
|||
Account not enabled: Cuenta no habilitada
|
||||
Inherited roles: Roles heredados
|
||||
Go to the user: Ir al usuario
|
||||
Log: Histórico
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<vn-log url="RoleLogs" origin-id="$ctrl.$params.id"></vn-log>
|
|
@ -0,0 +1,7 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
ngModule.vnComponent('vnRoleLog', {
|
||||
template: require('./index.html'),
|
||||
controller: Section,
|
||||
});
|
|
@ -20,12 +20,14 @@
|
|||
{"state": "account.card.roles", "icon": "group"},
|
||||
{"state": "account.card.mailForwarding", "icon": "forward"},
|
||||
{"state": "account.card.aliases", "icon": "email"},
|
||||
{"state": "account.card.privileges", "icon": "badge"}
|
||||
{"state": "account.card.privileges", "icon": "badge"},
|
||||
{"state": "account.card.log", "icon": "history"}
|
||||
],
|
||||
"role": [
|
||||
{"state": "account.role.card.basicData", "icon": "settings"},
|
||||
{"state": "account.role.card.subroles", "icon": "groups"},
|
||||
{"state": "account.role.card.inherited", "icon": "account_tree"}
|
||||
{"state": "account.role.card.inherited", "icon": "account_tree"},
|
||||
{"state": "account.role.card.log", "icon": "history"}
|
||||
],
|
||||
"alias": [
|
||||
{"state": "account.alias.card.basicData", "icon": "settings"},
|
||||
|
@ -80,6 +82,18 @@
|
|||
"description": "Basic data",
|
||||
"acl": ["hr"]
|
||||
},
|
||||
{
|
||||
"url" : "/log",
|
||||
"state": "account.card.log",
|
||||
"component": "vn-user-log",
|
||||
"description": "Log"
|
||||
},
|
||||
{
|
||||
"url" : "/log",
|
||||
"state": "account.role.card.log",
|
||||
"component": "vn-role-log",
|
||||
"description": "Log"
|
||||
},
|
||||
{
|
||||
"url": "/roles",
|
||||
"state": "account.card.roles",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<vn-log url="UserLogs" origin-id="$ctrl.$params.id"></vn-log>
|
|
@ -0,0 +1,7 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
ngModule.vnComponent('vnUserLog', {
|
||||
template: require('./index.html'),
|
||||
controller: Section,
|
||||
});
|
|
@ -1,6 +1,20 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('Update Claim', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
const newDate = Date.vnNew();
|
||||
const originalData = {
|
||||
ticketFk: 3,
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('Update Claim', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
const newDate = Date.vnNew();
|
||||
const original = {
|
||||
ticketFk: 3,
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ClaimBeginning",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim",
|
||||
"showField": "quantity"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimBeginning"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "ClaimDevelopment",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimDevelopment"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "ClaimDms",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimDms"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "ClaimEnd",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimEnd"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "ClaimObservation",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimObservation"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ClaimState",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"relation": "claim",
|
||||
"showField": "description"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claimState"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Claim",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClaimLog",
|
||||
"showField": "id"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "claim"
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
"name": "Address",
|
||||
"description": "Client addresses",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client",
|
||||
"showField": "nickname"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "address"
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
"name": "ClientContact",
|
||||
"description": "Client phone contacts",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client",
|
||||
"showField": "name"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "clientContact"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ClientDms",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"ClientLog",
|
||||
"relation": "client",
|
||||
"showField": "dmsFk"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "clientDms"
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
"name": "ClientObservation",
|
||||
"description": "Client notes",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "clientObservation"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ClientSample",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client",
|
||||
"showField": "type"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "clientSample"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Client",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"ClientLog",
|
||||
"showField": "id"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "client"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "Greuge",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client",
|
||||
"showField": "description"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "greuge"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Recovery",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ClientLog",
|
||||
"relation": "client"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "recovery"
|
||||
|
|
|
@ -64,3 +64,4 @@ Compensation Account: Cuenta para compensar
|
|||
Amount to return: Cantidad a devolver
|
||||
Delivered amount: Cantidad entregada
|
||||
Unpaid: Impagado
|
||||
There is no zona: No hay zona
|
|
@ -9,3 +9,4 @@ The country can't be empty: El país no puede quedar vacío
|
|||
The postcode has been created. You can save the data now: Se ha creado el código postal. Ahora puedes guardar los datos
|
||||
The city has been created: Se ha creado la ciudad
|
||||
The province has been created: Se ha creado la provincia
|
||||
Autonomy: Autonomia
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "Buy",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "EntryLog",
|
||||
"relation": "entry",
|
||||
"grabUser": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "buy"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "EntryObservation",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "EntryLog",
|
||||
"relation": "entry"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "entryObservation"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Entry",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"EntryLog",
|
||||
"grabUser": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "entry"
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('invoiceIn clone()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the cloned invoiceIn and also clone invoiceInDueDays and invoiceInTaxes if there are any referencing the invoiceIn', async() => {
|
||||
const userId = 1;
|
||||
const ctx = {
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "InvoiceInTax",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "InvoiceInLog",
|
||||
"relation": "invoiceIn"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "invoiceInTax"
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"name": "InvoiceIn",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "InvoiceInLog"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "invoiceIn"
|
||||
|
|
|
@ -56,7 +56,7 @@ module.exports = Self => {
|
|||
reference: invoiceOut.ref,
|
||||
recipientId: invoiceOut.clientFk
|
||||
});
|
||||
const stream = await invoiceReport.toPdfStream();
|
||||
const buffer = await invoiceReport.toPdfStream();
|
||||
|
||||
const issued = invoiceOut.issued;
|
||||
const year = issued.getFullYear().toString();
|
||||
|
@ -66,7 +66,7 @@ module.exports = Self => {
|
|||
const fileName = `${year}${invoiceOut.ref}.pdf`;
|
||||
|
||||
// Store invoice
|
||||
print.storage.write(stream, {
|
||||
await print.storage.write(buffer, {
|
||||
type: 'invoice',
|
||||
path: `${year}/${month}/${day}`,
|
||||
fileName: fileName
|
||||
|
|
|
@ -100,16 +100,23 @@ class Controller extends Section {
|
|||
};
|
||||
|
||||
this.$http.post(`InvoiceOuts/invoiceClient`, params)
|
||||
.then(() => this.invoiceNext())
|
||||
.catch(res => {
|
||||
this.errors.unshift({
|
||||
address,
|
||||
message: res.data.error.message
|
||||
});
|
||||
const message = res.data?.error?.message || res.message;
|
||||
if (res.status >= 400 && res.status < 500) {
|
||||
this.errors.unshift({address, message});
|
||||
this.invoiceNext();
|
||||
} else {
|
||||
this.invoicing = false;
|
||||
this.status = 'done';
|
||||
throw new UserError(`Critical invoicing error, proccess stopped`);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.addressIndex++;
|
||||
this.invoiceOut();
|
||||
});
|
||||
}
|
||||
|
||||
invoiceNext() {
|
||||
this.addressIndex++;
|
||||
this.invoiceOut();
|
||||
}
|
||||
|
||||
get nAddresses() {
|
||||
|
|
|
@ -18,3 +18,4 @@ Invoice out: Facturar
|
|||
One client: Un solo cliente
|
||||
Choose a valid client: Selecciona un cliente válido
|
||||
Stop: Parar
|
||||
Critical invoicing error, proccess stopped: Error crítico al facturar, proceso detenido
|
|
@ -1,9 +1,7 @@
|
|||
const axios = require('axios');
|
||||
const uuid = require('uuid');
|
||||
const fs = require('fs/promises');
|
||||
const {createWriteStream} = require('fs');
|
||||
const path = require('path');
|
||||
const gm = require('gm');
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('download', {
|
||||
|
@ -27,13 +25,9 @@ module.exports = Self => {
|
|||
const maxAttempts = 3;
|
||||
const collectionName = 'catalog';
|
||||
|
||||
const tx = await Self.beginTransaction({});
|
||||
|
||||
let tempFilePath;
|
||||
let queueRow;
|
||||
try {
|
||||
const myOptions = {transaction: tx};
|
||||
|
||||
queueRow = await Self.findOne(
|
||||
{
|
||||
fields: ['id', 'itemFk', 'url', 'attempts'],
|
||||
|
@ -44,58 +38,14 @@ module.exports = Self => {
|
|||
},
|
||||
},
|
||||
order: 'priority, attempts, updated',
|
||||
},
|
||||
myOptions
|
||||
}
|
||||
);
|
||||
|
||||
if (!queueRow) return;
|
||||
|
||||
const collection = await models.ImageCollection.findOne(
|
||||
{
|
||||
fields: [
|
||||
'id',
|
||||
'maxWidth',
|
||||
'maxHeight',
|
||||
'model',
|
||||
'property',
|
||||
],
|
||||
where: {name: collectionName},
|
||||
include: {
|
||||
relation: 'sizes',
|
||||
scope: {
|
||||
fields: ['width', 'height', 'crop'],
|
||||
},
|
||||
},
|
||||
},
|
||||
myOptions
|
||||
);
|
||||
|
||||
const fileName = `${uuid.v4()}.png`;
|
||||
const fileName = `${queueRow.itemFk}.png`;
|
||||
tempFilePath = path.join(tempPath, fileName);
|
||||
|
||||
// Insert image row
|
||||
await models.Image.create(
|
||||
{
|
||||
name: fileName,
|
||||
collectionFk: collectionName,
|
||||
updated: Date.vnNow(),
|
||||
},
|
||||
myOptions
|
||||
);
|
||||
|
||||
// Update item
|
||||
const model = models[collection.model];
|
||||
if (!model) throw new Error('No matching model found');
|
||||
|
||||
const item = await model.findById(queueRow.itemFk, null, myOptions);
|
||||
if (item) {
|
||||
await item.updateAttribute(
|
||||
collection.property,
|
||||
fileName,
|
||||
myOptions
|
||||
);
|
||||
}
|
||||
|
||||
// Download remote image
|
||||
const response = await axios.get(queueRow.url, {
|
||||
responseType: 'stream',
|
||||
|
@ -108,71 +58,22 @@ module.exports = Self => {
|
|||
writeStream.on('error', error => reject(error));
|
||||
});
|
||||
|
||||
// Resize
|
||||
const container = await models.ImageContainer.container(
|
||||
collectionName
|
||||
);
|
||||
const rootPath = container.client.root;
|
||||
const collectionDir = path.join(rootPath, collectionName);
|
||||
|
||||
// To max size
|
||||
const {maxWidth, maxHeight} = collection;
|
||||
const fullSizePath = path.join(collectionDir, 'full');
|
||||
const toFullSizePath = `${fullSizePath}/${fileName}`;
|
||||
|
||||
await fs.mkdir(fullSizePath, {recursive: true});
|
||||
await new Promise((resolve, reject) => {
|
||||
gm(tempFilePath)
|
||||
.resize(maxWidth, maxHeight, '>')
|
||||
.setFormat('png')
|
||||
.write(toFullSizePath, function(err) {
|
||||
if (err) reject(err);
|
||||
if (!err) resolve();
|
||||
});
|
||||
await models.Image.resize({
|
||||
collectionName: collectionName,
|
||||
srcFile: tempFilePath,
|
||||
fileName: fileName,
|
||||
entityId: queueRow.itemFk
|
||||
});
|
||||
|
||||
// To collection sizes
|
||||
for (const size of collection.sizes()) {
|
||||
const {width, height} = size;
|
||||
|
||||
const sizePath = path.join(collectionDir, `${width}x${height}`);
|
||||
const toSizePath = `${sizePath}/${fileName}`;
|
||||
|
||||
await fs.mkdir(sizePath, {recursive: true});
|
||||
await new Promise((resolve, reject) => {
|
||||
const gmInstance = gm(tempFilePath);
|
||||
|
||||
if (size.crop) {
|
||||
gmInstance
|
||||
.resize(width, height, '^')
|
||||
.gravity('Center')
|
||||
.crop(width, height);
|
||||
}
|
||||
|
||||
if (!size.crop) gmInstance.resize(width, height, '>');
|
||||
|
||||
gmInstance
|
||||
.setFormat('png')
|
||||
.write(toSizePath, function(err) {
|
||||
if (err) reject(err);
|
||||
if (!err) resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch (error) { }
|
||||
|
||||
await queueRow.destroy(myOptions);
|
||||
await queueRow.destroy();
|
||||
|
||||
// Restart queue
|
||||
Self.download();
|
||||
|
||||
await tx.commit();
|
||||
} catch (error) {
|
||||
await tx.rollback();
|
||||
|
||||
if (queueRow.attempts < maxAttempts) {
|
||||
await queueRow.updateAttributes({
|
||||
error: error,
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('item updateTaxes()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the taxClassFk is blank', async() => {
|
||||
const tx = await models.Item.beginTransaction({});
|
||||
const options = {transaction: tx};
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('tag onSubmit()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a tag', async() => {
|
||||
const tx = await models.Item.beginTransaction({});
|
||||
const options = {transaction: tx};
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ItemBarcode",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"relation": "item",
|
||||
"showField": "code"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "itemBarcode"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "ItemBotanical",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"relation": "item"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "itemBotanical"
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
},
|
||||
"isChecked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isChecked": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ItemTag",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"relation": "item",
|
||||
"showField": "value"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "itemTag"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "ItemTaxCountry",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"relation": "item",
|
||||
"showField": "countryFk"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "itemTaxCountry"
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"name": "Item",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"showField": "id",
|
||||
"grabUser": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "item"
|
||||
|
|
|
@ -1,136 +1,215 @@
|
|||
<vn-crud-model
|
||||
url="Tags"
|
||||
fields="['id','name','isFree', 'sourceTable']"
|
||||
data="tags"
|
||||
auto-load="true">
|
||||
</vn-crud-model>
|
||||
<div class="search-panel">
|
||||
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
|
||||
<vn-horizontal>
|
||||
<vn-textfield
|
||||
vn-one
|
||||
label="General search"
|
||||
ng-model="filter.search"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
url="ItemCategories"
|
||||
label="Category"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
ng-model="filter.categoryFk">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete vn-one
|
||||
url="ItemTypes"
|
||||
label="Type"
|
||||
where="{categoryFk: filter.categoryFk}"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
ng-model="filter.typeFk"
|
||||
fields="['categoryFk']"
|
||||
include="'category'">
|
||||
<tpl-item>
|
||||
<div>{{name}}</div>
|
||||
<div class="text-caption text-secondary">
|
||||
{{category.name}}
|
||||
</div>
|
||||
</tpl-item>
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.buyerFk"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: {inq: ['logistic', 'buyer']}}"
|
||||
label="Buyer">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
label="Warehouse"
|
||||
ng-model="filter.warehouseFk"
|
||||
url="Warehouses">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Started"
|
||||
ng-model="filter.started">
|
||||
</vn-date-picker>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Ended"
|
||||
ng-model="filter.ended">
|
||||
</vn-date-picker>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-check vn-one
|
||||
triple-state="true"
|
||||
label="For me"
|
||||
ng-model="filter.mine">
|
||||
</vn-check>
|
||||
<vn-check vn-one
|
||||
triple-state="true"
|
||||
label="Minimum price"
|
||||
ng-model="filter.hasMinPrice">
|
||||
</vn-check>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="vn-pt-sm">
|
||||
<vn-one class="text-subtitle1" translate>
|
||||
Tags
|
||||
</vn-one>
|
||||
<vn-icon-button
|
||||
vn-none
|
||||
vn-bind="+"
|
||||
vn-tooltip="Add tag"
|
||||
icon="add_circle"
|
||||
ng-click="filter.tags.push({})">
|
||||
</vn-icon-button>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal ng-repeat="itemTag in filter.tags">
|
||||
<vn-autocomplete vn-two vn-id="tag"
|
||||
label="Tag"
|
||||
initial-data="itemTag.tag"
|
||||
ng-model="itemTag.tagFk"
|
||||
data="tags"
|
||||
show-field="name"
|
||||
on-change="itemTag.value = null"
|
||||
rule>
|
||||
</vn-autocomplete>
|
||||
<vn-textfield vn-three
|
||||
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
|
||||
vn-id="text"
|
||||
label="Value"
|
||||
ng-model="itemTag.value"
|
||||
rule>
|
||||
</vn-textfield>
|
||||
<vn-autocomplete vn-three
|
||||
ng-show="tag.selection.isFree === false"
|
||||
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
|
||||
search-function="{value: $search}"
|
||||
label="Value"
|
||||
ng-model="itemTag.value"
|
||||
show-field="value"
|
||||
value-field="value"
|
||||
rule>
|
||||
</vn-autocomplete>
|
||||
<vn-icon-button
|
||||
vn-none
|
||||
vn-tooltip="Remove tag"
|
||||
icon="delete"
|
||||
ng-click="filter.tags.splice($index, 1)"
|
||||
tabindex="-1">
|
||||
</vn-icon-button>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="vn-mt-lg">
|
||||
<vn-submit label="Search"></vn-submit>
|
||||
</vn-horizontal>
|
||||
</form>
|
||||
</div>
|
||||
<vn-crud-model url="Tags" fields="['id','name','isFree']" data="$ctrl.tags" auto-load="true" />
|
||||
<vn-crud-model url="ItemCategories" data="categories" auto-load="true" />
|
||||
<vn-side-menu side="right">
|
||||
<vn-horizontal class="input">
|
||||
<vn-textfield
|
||||
label="General search"
|
||||
ng-model="$ctrl.filter.search"
|
||||
info="Search items by id, name or barcode"
|
||||
vn-focus
|
||||
ng-keydown="$ctrl.onKeyPress($event)">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="item-category">
|
||||
<vn-autocomplete
|
||||
vn-id="category"
|
||||
data="categories"
|
||||
ng-model="$ctrl.filter.categoryFk"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
label="Category">
|
||||
</vn-autocomplete>
|
||||
<vn-one ng-repeat="category in categories">
|
||||
<vn-icon
|
||||
ng-class="{'active': $ctrl.filter.categoryFk == category.id}"
|
||||
icon="{{::category.icon}}"
|
||||
vn-tooltip="{{::category.name}}"
|
||||
ng-click="$ctrl.changeCategory(category.id)">
|
||||
</vn-icon>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
<vn-vertical class="input">
|
||||
<vn-autocomplete
|
||||
vn-id="type"
|
||||
disabled="!$ctrl.filter.categoryFk"
|
||||
url="ItemTypes"
|
||||
label="Type"
|
||||
where="{categoryFk: $ctrl.filter.categoryFk}"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
ng-model="$ctrl.filter.typeFk"
|
||||
fields="['categoryFk']"
|
||||
include="'category'"
|
||||
on-change="$ctrl.addFilters()">
|
||||
<tpl-item>
|
||||
<div>{{name}}</div>
|
||||
<div class="text-caption text-secondary">
|
||||
{{category.name}}
|
||||
</div>
|
||||
</tpl-item>
|
||||
</vn-autocomplete>
|
||||
</vn-vertical>
|
||||
<vn-horizontal class="input horizontal">
|
||||
<vn-autocomplete
|
||||
vn-id="buyer"
|
||||
disabled="false"
|
||||
ng-model="$ctrl.filter.buyerFk"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: {inq: ['logistic', 'buyer']}}"
|
||||
label="Buyer"
|
||||
on-change="$ctrl.addFilters()">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
vn-id="warehouse"
|
||||
label="Warehouse"
|
||||
ng-model="$ctrl.filter.warehouseFk"
|
||||
url="Warehouses"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
on-change="$ctrl.addFilters()">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="input horizontal">
|
||||
<vn-date-picker
|
||||
label="From"
|
||||
ng-model="$ctrl.filter.started"
|
||||
on-change="$ctrl.addFilters()">
|
||||
</vn-date-picker>
|
||||
<vn-date-picker
|
||||
label="To"
|
||||
ng-model="$ctrl.filter.ended"
|
||||
on-change="$ctrl.addFilters()">
|
||||
</vn-date-picker>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="input horizontal">
|
||||
<vn-check
|
||||
label="For me"
|
||||
ng-model="$ctrl.filter.mine"
|
||||
triple-state="true"
|
||||
ng-click="$ctrl.addFilters()">
|
||||
</vn-check>
|
||||
<vn-check
|
||||
label="Minimum price"
|
||||
ng-model="$ctrl.filter.hasMinPrice"
|
||||
triple-state="true"
|
||||
ng-click="$ctrl.addFilters()">
|
||||
</vn-check>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="tags">
|
||||
<vn-one class="text-subtitle1" translate> Tags </vn-one>
|
||||
<vn-icon-button
|
||||
vn-none
|
||||
vn-tooltip="Add tag"
|
||||
icon="add_circle"
|
||||
ng-click="$ctrl.filter.tags.push({})">
|
||||
</vn-icon-button>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="tags horizontal" ng-repeat="itemTag in $ctrl.filter.tags">
|
||||
<vn-autocomplete
|
||||
vn-id="tag"
|
||||
data="$ctrl.tags"
|
||||
ng-model="itemTag.tagFk"
|
||||
show-field="name"
|
||||
label="Tag"
|
||||
on-change="itemTag.value = null">
|
||||
</vn-autocomplete>
|
||||
<vn-textfield
|
||||
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
|
||||
label="Value"
|
||||
ng-model="itemTag.value"
|
||||
ng-keydown="$ctrl.onKeyPress($event)">
|
||||
</vn-textfield>
|
||||
<vn-autocomplete
|
||||
ng-show="tag.selection.isFree === false"
|
||||
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
|
||||
search-function="{value: $search}"
|
||||
label="Value"
|
||||
ng-model="itemTag.value"
|
||||
show-field="value"
|
||||
value-field="value"
|
||||
on-change="$ctrl.addFilters()">
|
||||
</vn-autocomplete>
|
||||
<vn-icon-button
|
||||
vn-none
|
||||
vn-tooltip="Remove tag"
|
||||
icon="delete"
|
||||
ng-click="$ctrl.removeTag(itemTag)">
|
||||
</vn-icon-button>
|
||||
</vn-horizontal>
|
||||
<div class="chips">
|
||||
<vn-chip
|
||||
ng-if="$ctrl.filter.search"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('search')"
|
||||
class="colored">
|
||||
<span>Id/{{$ctrl.$t('Name')}}: {{$ctrl.filter.search}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="category.selection"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('categoryFk')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Category')}}: {{category.selection.name}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="type.selection"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('typeFk')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Type')}}: {{type.selection.name}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="buyer.selection"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('buyerFk')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Buyer')}}: {{buyer.selection.nickname}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="warehouse.selection"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('warehouseFk')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Warehouse')}}: {{warehouse.selection.name}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="$ctrl.filter.started"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('started')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Started')}}: {{$ctrl.filter.started | date:'dd/MM/yyyy'}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="$ctrl.filter.ended"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('ended')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Ended')}}: {{$ctrl.filter.ended | date:'dd/MM/yyyy'}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="$ctrl.filter.mine != null"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('mine')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('For me')}}: {{$ctrl.filter.mine ? '✓' : '✗'}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-if="$ctrl.filter.hasMinPrice != null"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeItemFilter('hasMinPrice')"
|
||||
class="colored">
|
||||
<span>{{$ctrl.$t('Minimum price')}}: {{$ctrl.filter.hasMinPrice ? '✓' : '✗'}}</span>
|
||||
</vn-chip>
|
||||
<vn-chip
|
||||
ng-repeat="chipTag in $ctrl.filter.tags"
|
||||
removable="true"
|
||||
on-remove="$ctrl.removeTag(chipTag)"
|
||||
class="colored"
|
||||
ng-if="chipTag.value">
|
||||
<span>{{$ctrl.showTagInfo(chipTag)}}</span>
|
||||
</vn-chip>
|
||||
</div>
|
||||
</vn-side-menu>
|
||||
|
|
|
@ -1,19 +1,60 @@
|
|||
import ngModule from '../module';
|
||||
import SearchPanel from 'core/components/searchbar/search-panel';
|
||||
import './style.scss';
|
||||
|
||||
class Controller extends SearchPanel {
|
||||
get filter() {
|
||||
return this.$.filter;
|
||||
constructor($element, $) {
|
||||
super($element, $);
|
||||
}
|
||||
|
||||
set filter(value = {}) {
|
||||
if (!value.tags) value.tags = [{}];
|
||||
$onInit() {
|
||||
this.filter = {
|
||||
tags: []
|
||||
};
|
||||
}
|
||||
|
||||
this.$.filter = value;
|
||||
changeCategory(id) {
|
||||
if (this.filter.categoryFk != id) {
|
||||
this.filter.categoryFk = id;
|
||||
this.addFilters();
|
||||
}
|
||||
}
|
||||
|
||||
removeItemFilter(param) {
|
||||
this.filter[param] = null;
|
||||
if (param == 'categoryFk') this.filter['typeFk'] = null;
|
||||
this.addFilters();
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
const index = this.filter.tags.indexOf(tag);
|
||||
if (index > -1) this.filter.tags.splice(index, 1);
|
||||
this.addFilters();
|
||||
}
|
||||
|
||||
onKeyPress($event) {
|
||||
if ($event.key === 'Enter')
|
||||
this.addFilters();
|
||||
}
|
||||
|
||||
addFilters() {
|
||||
for (let i = 0; i < this.filter.tags.length; i++) {
|
||||
if (!this.filter.tags[i].value)
|
||||
this.filter.tags.splice(i, 1);
|
||||
}
|
||||
return this.model.addFilter({}, this.filter);
|
||||
}
|
||||
|
||||
showTagInfo(itemTag) {
|
||||
if (!itemTag.tagFk) return itemTag.value;
|
||||
return `${this.tags.find(tag => tag.id == itemTag.tagFk).name}: ${itemTag.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnFixedPriceSearchPanel', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller
|
||||
controller: Controller,
|
||||
bindings: {
|
||||
model: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import './index.js';
|
||||
|
||||
describe('Item', () => {
|
||||
describe('Component vnFixedPriceSearchPanel', () => {
|
||||
let $element;
|
||||
let controller;
|
||||
|
||||
beforeEach(ngModule('item'));
|
||||
|
||||
beforeEach(angular.mock.inject($componentController => {
|
||||
$element = angular.element(`<vn-fixed-price-search-panel></vn-fixed-price-search-panel>`);
|
||||
controller = $componentController('vnFixedPriceSearchPanel', {$element});
|
||||
controller.model = {addFilter: () => {}};
|
||||
}));
|
||||
|
||||
describe('removeItemFilter()', () => {
|
||||
it(`should remove param from filter`, () => {
|
||||
controller.filter = {tags: [], categoryFk: 1, typeFk: 1};
|
||||
const expectFilter = {tags: [], categoryFk: null, typeFk: null};
|
||||
|
||||
controller.removeItemFilter('categoryFk');
|
||||
|
||||
expect(controller.filter).toEqual(expectFilter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTag()', () => {
|
||||
it(`should remove tag from filter`, () => {
|
||||
const tag = {tagFk: 1, value: 'Value'};
|
||||
controller.filter = {tags: [tag]};
|
||||
const expectFilter = {tags: []};
|
||||
|
||||
controller.removeTag(tag);
|
||||
|
||||
expect(controller.filter).toEqual(expectFilter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showTagInfo()', () => {
|
||||
it(`should show tag value`, () => {
|
||||
const tag = {value: 'Value'};
|
||||
const result = controller.showTagInfo(tag);
|
||||
|
||||
expect(result).toEqual('Value');
|
||||
});
|
||||
|
||||
it(`should show tag name and value`, () => {
|
||||
const tag = {tagFk: 1, value: 'Value'};
|
||||
controller.tags = [{id: 1, name: 'tagName'}];
|
||||
const result = controller.showTagInfo(tag);
|
||||
|
||||
expect(result).toEqual('tagName: Value');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
@import "variables";
|
||||
|
||||
vn-fixed-price-search-panel vn-side-menu {
|
||||
.menu {
|
||||
min-width: $right-menu-width;
|
||||
}
|
||||
& > div {
|
||||
.input {
|
||||
padding-left: $spacing-md;
|
||||
padding-right: $spacing-md;
|
||||
border-color: $color-spacer;
|
||||
border-bottom: $border-thin;
|
||||
}
|
||||
.horizontal {
|
||||
padding-left: $spacing-md;
|
||||
padding-right: $spacing-md;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: $spacing-sm;
|
||||
align-items: center;
|
||||
}
|
||||
.tags {
|
||||
padding: $spacing-md;
|
||||
padding-bottom: 0%;
|
||||
padding-top: 0%;
|
||||
align-items: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: $spacing-md;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
border-color: $color-spacer;
|
||||
border-top: $border-thin;
|
||||
}
|
||||
.item-category {
|
||||
padding: $spacing-sm;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
vn-autocomplete[vn-id="category"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > vn-one {
|
||||
padding: $spacing-sm;
|
||||
min-width: 33.33%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
& > vn-icon {
|
||||
padding: $spacing-sm;
|
||||
background-color: $color-font-secondary;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: $color-main;
|
||||
color: #fff;
|
||||
}
|
||||
& > i:before {
|
||||
font-size: 2.6rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,16 +14,10 @@
|
|||
order="name">
|
||||
</vn-crud-model>
|
||||
<vn-portal slot="topbar">
|
||||
<vn-searchbar
|
||||
auto-state="false"
|
||||
panel="vn-fixed-price-search-panel"
|
||||
info="Search prices by item ID or code"
|
||||
suggested-filter="$ctrl.filterParams"
|
||||
filter="$ctrl.filterParams"
|
||||
placeholder="Search fixed prices"
|
||||
model="model">
|
||||
</vn-searchbar>
|
||||
</vn-portal>
|
||||
<vn-fixed-price-search-panel
|
||||
model="model">
|
||||
</vn-fixed-price-search-panel>
|
||||
<div class="vn-w-xl">
|
||||
<vn-card>
|
||||
<smart-table
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('AgencyTerm createInvoiceIn()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
const rows = [
|
||||
{
|
||||
routeFk: 2,
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Route",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"RouteLog",
|
||||
"grabUser": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "route"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Shelving",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ShelvingLog",
|
||||
"showField": "id"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "shelving"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "SupplierAccount",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"SupplierLog",
|
||||
"relation": "supplier"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "supplierAccount"
|
||||
|
|
|
@ -2,11 +2,6 @@
|
|||
"name": "SupplierAddress",
|
||||
"description": "Supplier addresses",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "SupplierLog",
|
||||
"relation": "supplier",
|
||||
"showField": "name"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "supplierAddress"
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "SupplierContact",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"SupplierLog",
|
||||
"relation": "supplier"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "supplierContact"
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
{
|
||||
"name": "Supplier",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model":"SupplierLog"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "supplier"
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
type="number"
|
||||
label="Km Price"
|
||||
ng-model="$ctrl.supplierAgencyTerm.kmPrice"
|
||||
step="0.01"
|
||||
rule>
|
||||
</vn-input-number>
|
||||
<vn-input-number
|
||||
|
|
|
@ -47,9 +47,11 @@
|
|||
<vn-tbody>
|
||||
<vn-tr ng-repeat="buy in entry.buys">
|
||||
<vn-td expand>
|
||||
<span>
|
||||
{{::buy.itemName}}
|
||||
</span>
|
||||
<span
|
||||
vn-click-stop="itemDescriptor.show($event, buy.id)"
|
||||
class="link">
|
||||
{{::buy.itemName}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td vn-fetched-tags>
|
||||
<div>
|
||||
|
@ -89,3 +91,7 @@
|
|||
message="The consumption report will be sent"
|
||||
on-accept="$ctrl.sendEmail()">
|
||||
</vn-confirm>
|
||||
<vn-item-descriptor-popover
|
||||
vn-id="item-descriptor"
|
||||
warehouse-fk="$ctrl.vnConfig.warehouseFk">
|
||||
</vn-item-descriptor-popover>
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('ticket deleteExpeditions()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete the selected expeditions', async() => {
|
||||
const tx = await models.Expedition.beginTransaction({});
|
||||
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('ticket moveExpeditions()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should move the selected expeditions to new ticket', async() => {
|
||||
const tx = await models.Expedition.beginTransaction({});
|
||||
const ctx = {
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('ticket-request confirm()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
let ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
const models = require('vn-loopback/server/server').models;
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('ticket-request deny()', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the denied ticket request', async() => {
|
||||
const tx = await models.TicketRequest.beginTransaction({});
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ module.exports = async function(Self, tickets, reqArgs = {}) {
|
|||
const fileName = `${year}${invoiceOut.ref}.pdf`;
|
||||
|
||||
// Store invoice
|
||||
storage.write(stream, {
|
||||
await storage.write(stream, {
|
||||
type: 'invoice',
|
||||
path: `${year}/${month}/${day}`,
|
||||
fileName: fileName
|
||||
|
|
|
@ -34,6 +34,8 @@ module.exports = Self => {
|
|||
const models = Self.app.models;
|
||||
const myOptions = {};
|
||||
let tx;
|
||||
let dms;
|
||||
let gestDocCreated = false;
|
||||
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
@ -96,11 +98,12 @@ module.exports = Self => {
|
|||
warehouseId: ticket.warehouseFk,
|
||||
companyId: ticket.companyFk,
|
||||
dmsTypeId: dmsType.id,
|
||||
reference: id,
|
||||
description: `Ticket ${id} Cliente ${ticket.client().name} Ruta ${ticket.route().id}`,
|
||||
reference: '',
|
||||
description: `Firma del cliente - Ruta ${ticket.route().id}`,
|
||||
hasFile: true
|
||||
};
|
||||
await models.Ticket.uploadFile(ctxUploadFile, id, myOptions);
|
||||
dms = await models.Dms.uploadFile(ctxUploadFile, myOptions);
|
||||
gestDocCreated = true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -118,12 +121,16 @@ module.exports = Self => {
|
|||
throw new UserError('This ticket cannot be signed because it has not been boxed');
|
||||
else if (!await gestDocExists(args.tickets[i])) {
|
||||
if (args.location) setLocation(args.tickets[i]);
|
||||
await createGestDoc(args.tickets[i]);
|
||||
if (!gestDocCreated) await createGestDoc(args.tickets[i]);
|
||||
await models.TicketDms.create({ticketFk: args.tickets[i], dmsFk: dms[0].id}, myOptions);
|
||||
const ticket = await models.Ticket.findById(args.tickets[i], null, myOptions);
|
||||
await ticket.updateAttribute('isSigned', true, myOptions);
|
||||
await Self.rawSql(`CALL vn.ticket_setState(?, ?)`, [args.tickets[i], 'DELIVERED'], myOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if (tx) await tx.commit();
|
||||
return;
|
||||
} catch (e) {
|
||||
if (tx) await tx.rollback();
|
||||
throw e;
|
||||
|
|
|
@ -17,6 +17,17 @@ describe('ticket componentUpdate()', () => {
|
|||
let componentValue;
|
||||
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
const deliveryComponenet = await models.Component.findOne({where: {code: 'delivery'}});
|
||||
deliveryComponentId = deliveryComponenet.id;
|
||||
componentOfSaleSeven = `SELECT value
|
||||
|
@ -180,9 +191,6 @@ describe('ticket componentUpdate()', () => {
|
|||
}
|
||||
};
|
||||
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: ctx.req
|
||||
});
|
||||
const oldTicket = await models.Ticket.findById(ticketID, null, options);
|
||||
|
||||
await models.Ticket.componentUpdate(ctx, options);
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "Expedition",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "TicketLog",
|
||||
"relation": "ticket"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "expedition"
|
||||
|
@ -59,4 +55,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"name": "Sale",
|
||||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "TicketLog",
|
||||
"relation": "ticket",
|
||||
"showField": "concept",
|
||||
"grabUser": true
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "sale"
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const LoopBackContext = require('loopback-context');
|
||||
|
||||
describe('ticket model TicketTracking', () => {
|
||||
beforeAll(async() => {
|
||||
const activeCtx = {
|
||||
accessToken: {userId: 9},
|
||||
http: {
|
||||
req: {
|
||||
headers: {origin: 'http://localhost'}
|
||||
}
|
||||
}
|
||||
};
|
||||
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||
active: activeCtx
|
||||
});
|
||||
});
|
||||
let ticketTrackingId;
|
||||
|
||||
afterAll(async() => {
|
||||
|
|
|
@ -14,6 +14,15 @@
|
|||
},
|
||||
"scopeDays": {
|
||||
"type": "number"
|
||||
},
|
||||
"pickingDelay": {
|
||||
"type": "number"
|
||||
},
|
||||
"packagingInvoicingDated": {
|
||||
"type": "date"
|
||||
},
|
||||
"daysForWarningClaim": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue