Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5475-email_2fa

This commit is contained in:
Joan Sanchez 2023-04-18 11:52:17 +02:00
commit cdf092a989
131 changed files with 1459 additions and 1472 deletions

View File

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

View File

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

View File

@ -131,9 +131,6 @@
"UserConfigView": {
"dataSource": "vn"
},
"UserLog": {
"dataSource": "vn"
},
"Warehouse": {
"dataSource": "vn"
},

View File

@ -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']
fields: ['width', 'height', 'crop'],
},
},
}
}
}, myOptions);
);
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'
};
}
const resizeOpts = {
withoutEnlargement: true,
fit: 'inside'
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(imgSrc, sharpOptions)
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
.png()
.toFile(dstFile);
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'
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(imgSrc, sharpOptions)
.resize(size.width, size.height, resizeOpts)
.png()
.toFile(dstFile);
updated: Date.vnNow() / 1000,
}
);
// Update entity image file name
const model = models[collection.model];
if (!model) throw new Error('No matching model found');
if (!model)
throw new Error('Matching model not found');
const item = await model.findById(entityId, null, myOptions);
if (item) {
await item.updateAttribute(
const entity = await model.findById(entityId);
if (entity) {
await entity.updateAttribute(
collection.property,
fileName,
myOptions
fileName
);
}
if (fs.existsSync(srcFilePath))
await fs.unlink(srcFilePath);
// Resize
const container = await models.ImageContainer.container(
collectionName
);
const rootPath = container.client.root;
const collectionDir = path.join(rootPath, collectionName);
await tx.commit();
// To max size
const {maxWidth, maxHeight} = collection;
const fullSizePath = path.join(collectionDir, 'full');
const toFullSizePath = `${fullSizePath}/${fileName}`;
return newImage;
} catch (e) {
await tx.rollback();
throw e;
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();
});
});
// 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(srcFile);
if (size.crop) {
gmInstance
.resize(width, height, '^')
.gravity('Center')
.crop(width, height);
}
if (!size.crop) gmInstance.resize(width, height, '>');
gmInstance
.setFormat('png')
.quality(100)
.write(toSizePath, function(err) {
if (err) reject(err);
if (!err) resolve();
});
});
}
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
"base": "VnModel",
"options": {
"mysql": {
"table": "userLog"
"table": "account.userLog"
}
},
"properties": {

View File

@ -19,3 +19,5 @@ import './ldap';
import './samba';
import './accounts';
import './privileges';
import './user-log';
import './role-log';

View File

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

View File

@ -0,0 +1 @@
<vn-log url="RoleLogs" origin-id="$ctrl.$params.id"></vn-log>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
ngModule.vnComponent('vnRoleLog', {
template: require('./index.html'),
controller: Section,
});

View File

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

View File

@ -0,0 +1 @@
<vn-log url="UserLogs" origin-id="$ctrl.$params.id"></vn-log>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import Section from 'salix/components/section';
ngModule.vnComponent('vnUserLog', {
template: require('./index.html'),
controller: Section,
});

View File

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

View File

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

View File

@ -1,11 +1,6 @@
{
"name": "ClaimBeginning",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim",
"showField": "quantity"
},
"options": {
"mysql": {
"table": "claimBeginning"

View File

@ -1,10 +1,6 @@
{
"name": "ClaimDevelopment",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim"
},
"options": {
"mysql": {
"table": "claimDevelopment"

View File

@ -1,10 +1,6 @@
{
"name": "ClaimDms",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim"
},
"options": {
"mysql": {
"table": "claimDms"

View File

@ -1,10 +1,6 @@
{
"name": "ClaimEnd",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim"
},
"options": {
"mysql": {
"table": "claimEnd"

View File

@ -1,10 +1,6 @@
{
"name": "ClaimObservation",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim"
},
"options": {
"mysql": {
"table": "claimObservation"

View File

@ -1,11 +1,6 @@
{
"name": "ClaimState",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"relation": "claim",
"showField": "description"
},
"options": {
"mysql": {
"table": "claimState"

View File

@ -1,10 +1,6 @@
{
"name": "Claim",
"base": "Loggable",
"log": {
"model": "ClaimLog",
"showField": "id"
},
"options": {
"mysql": {
"table": "claim"

View File

@ -2,11 +2,6 @@
"name": "Address",
"description": "Client addresses",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client",
"showField": "nickname"
},
"options": {
"mysql": {
"table": "address"

View File

@ -2,11 +2,6 @@
"name": "ClientContact",
"description": "Client phone contacts",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client",
"showField": "name"
},
"options": {
"mysql": {
"table": "clientContact"

View File

@ -1,11 +1,6 @@
{
"name": "ClientDms",
"base": "Loggable",
"log": {
"model":"ClientLog",
"relation": "client",
"showField": "dmsFk"
},
"options": {
"mysql": {
"table": "clientDms"

View File

@ -2,10 +2,6 @@
"name": "ClientObservation",
"description": "Client notes",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client"
},
"options": {
"mysql": {
"table": "clientObservation"

View File

@ -1,11 +1,6 @@
{
"name": "ClientSample",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client",
"showField": "type"
},
"options": {
"mysql": {
"table": "clientSample"

View File

@ -1,10 +1,6 @@
{
"name": "Client",
"base": "Loggable",
"log": {
"model":"ClientLog",
"showField": "id"
},
"options": {
"mysql": {
"table": "client"

View File

@ -1,11 +1,6 @@
{
"name": "Greuge",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client",
"showField": "description"
},
"options": {
"mysql": {
"table": "greuge"

View File

@ -1,10 +1,6 @@
{
"name": "Recovery",
"base": "Loggable",
"log": {
"model": "ClientLog",
"relation": "client"
},
"options": {
"mysql": {
"table": "recovery"

View File

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

View File

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

View File

@ -1,11 +1,6 @@
{
"name": "Buy",
"base": "Loggable",
"log": {
"model": "EntryLog",
"relation": "entry",
"grabUser": true
},
"options": {
"mysql": {
"table": "buy"

View File

@ -1,10 +1,6 @@
{
"name": "EntryObservation",
"base": "Loggable",
"log": {
"model": "EntryLog",
"relation": "entry"
},
"options": {
"mysql": {
"table": "entryObservation"

View File

@ -1,10 +1,6 @@
{
"name": "Entry",
"base": "Loggable",
"log": {
"model":"EntryLog",
"grabUser": true
},
"options": {
"mysql": {
"table": "entry"

View File

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

View File

@ -1,10 +1,6 @@
{
"name": "InvoiceInTax",
"base": "Loggable",
"log": {
"model": "InvoiceInLog",
"relation": "invoiceIn"
},
"options": {
"mysql": {
"table": "invoiceInTax"

View File

@ -1,9 +1,6 @@
{
"name": "InvoiceIn",
"base": "Loggable",
"log": {
"model": "InvoiceInLog"
},
"options": {
"mysql": {
"table": "invoiceIn"

View File

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

View File

@ -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(() => {
}
invoiceNext() {
this.addressIndex++;
this.invoiceOut();
});
}
get nAddresses() {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,6 @@
{
"name": "ItemBarcode",
"base": "Loggable",
"log": {
"model": "ItemLog",
"relation": "item",
"showField": "code"
},
"options": {
"mysql": {
"table": "itemBarcode"

View File

@ -1,10 +1,6 @@
{
"name": "ItemBotanical",
"base": "Loggable",
"log": {
"model": "ItemLog",
"relation": "item"
},
"options": {
"mysql": {
"table": "itemBotanical"

View File

@ -1,11 +1,6 @@
{
"name": "ItemTag",
"base": "Loggable",
"log": {
"model": "ItemLog",
"relation": "item",
"showField": "value"
},
"options": {
"mysql": {
"table": "itemTag"

View File

@ -1,11 +1,6 @@
{
"name": "ItemTaxCountry",
"base": "Loggable",
"log": {
"model": "ItemLog",
"relation": "item",
"showField": "countryFk"
},
"options": {
"mysql": {
"table": "itemTaxCountry"

View File

@ -1,11 +1,6 @@
{
"name": "Item",
"base": "Loggable",
"log": {
"model": "ItemLog",
"showField": "id",
"grabUser": true
},
"options": {
"mysql": {
"table": "item"

View File

@ -1,37 +1,46 @@
<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-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
vn-one
label="General search"
ng-model="filter.search"
vn-focus>
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>
<vn-horizontal class="item-category">
<vn-autocomplete
vn-one
url="ItemCategories"
label="Category"
vn-id="category"
data="categories"
ng-model="$ctrl.filter.categoryFk"
show-field="name"
value-field="id"
ng-model="filter.categoryFk">
label="Category">
</vn-autocomplete>
<vn-autocomplete vn-one
<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: filter.categoryFk}"
where="{categoryFk: $ctrl.filter.categoryFk}"
show-field="name"
value-field="id"
ng-model="filter.typeFk"
ng-model="$ctrl.filter.typeFk"
fields="['categoryFk']"
include="'category'">
include="'category'"
on-change="$ctrl.addFilters()">
<tpl-item>
<div>{{name}}</div>
<div class="text-caption text-secondary">
@ -39,79 +48,81 @@
</div>
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
</vn-vertical>
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-one
ng-model="filter.buyerFk"
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">
label="Buyer"
on-change="$ctrl.addFilters()">
</vn-autocomplete>
<vn-autocomplete
vn-one
vn-id="warehouse"
label="Warehouse"
ng-model="filter.warehouseFk"
url="Warehouses">
ng-model="$ctrl.filter.warehouseFk"
url="Warehouses"
show-field="name"
value-field="id"
on-change="$ctrl.addFilters()">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="input horizontal">
<vn-date-picker
vn-one
label="Started"
ng-model="filter.started">
label="From"
ng-model="$ctrl.filter.started"
on-change="$ctrl.addFilters()">
</vn-date-picker>
<vn-date-picker
vn-one
label="Ended"
ng-model="filter.ended">
label="To"
ng-model="$ctrl.filter.ended"
on-change="$ctrl.addFilters()">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-check vn-one
triple-state="true"
<vn-horizontal class="input horizontal">
<vn-check
label="For me"
ng-model="filter.mine">
</vn-check>
<vn-check vn-one
ng-model="$ctrl.filter.mine"
triple-state="true"
ng-click="$ctrl.addFilters()">
</vn-check>
<vn-check
label="Minimum price"
ng-model="filter.hasMinPrice">
ng-model="$ctrl.filter.hasMinPrice"
triple-state="true"
ng-click="$ctrl.addFilters()">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-pt-sm">
<vn-one class="text-subtitle1" translate>
Tags
</vn-one>
<vn-horizontal class="tags">
<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({})">
ng-click="$ctrl.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"
<vn-horizontal class="tags horizontal" ng-repeat="itemTag in $ctrl.filter.tags">
<vn-autocomplete
vn-id="tag"
data="$ctrl.tags"
ng-model="itemTag.tagFk"
data="tags"
show-field="name"
on-change="itemTag.value = null"
rule>
label="Tag"
on-change="itemTag.value = null">
</vn-autocomplete>
<vn-textfield vn-three
<vn-textfield
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
vn-id="text"
label="Value"
ng-model="itemTag.value"
rule>
ng-keydown="$ctrl.onKeyPress($event)">
</vn-textfield>
<vn-autocomplete vn-three
<vn-autocomplete
ng-show="tag.selection.isFree === false"
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
search-function="{value: $search}"
@ -119,18 +130,86 @@
ng-model="itemTag.value"
show-field="value"
value-field="value"
rule>
on-change="$ctrl.addFilters()">
</vn-autocomplete>
<vn-icon-button
vn-none
vn-tooltip="Remove tag"
icon="delete"
ng-click="filter.tags.splice($index, 1)"
tabindex="-1">
ng-click="$ctrl.removeTag(itemTag)">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,6 @@
{
"name": "Route",
"base": "Loggable",
"log": {
"model":"RouteLog",
"grabUser": true
},
"options": {
"mysql": {
"table": "route"

View File

@ -1,10 +1,6 @@
{
"name": "Shelving",
"base": "Loggable",
"log": {
"model": "ShelvingLog",
"showField": "id"
},
"options": {
"mysql": {
"table": "shelving"

View File

@ -1,10 +1,6 @@
{
"name": "SupplierAccount",
"base": "Loggable",
"log": {
"model":"SupplierLog",
"relation": "supplier"
},
"options": {
"mysql": {
"table": "supplierAccount"

View File

@ -2,11 +2,6 @@
"name": "SupplierAddress",
"description": "Supplier addresses",
"base": "Loggable",
"log": {
"model": "SupplierLog",
"relation": "supplier",
"showField": "name"
},
"options": {
"mysql": {
"table": "supplierAddress"

View File

@ -1,10 +1,6 @@
{
"name": "SupplierContact",
"base": "Loggable",
"log": {
"model":"SupplierLog",
"relation": "supplier"
},
"options": {
"mysql": {
"table": "supplierContact"

View File

@ -1,9 +1,6 @@
{
"name": "Supplier",
"base": "Loggable",
"log": {
"model":"SupplierLog"
},
"options": {
"mysql": {
"table": "supplier"

View File

@ -41,6 +41,7 @@
type="number"
label="Km Price"
ng-model="$ctrl.supplierAgencyTerm.kmPrice"
step="0.01"
rule>
</vn-input-number>
<vn-input-number

View File

@ -47,7 +47,9 @@
<vn-tbody>
<vn-tr ng-repeat="buy in entry.buys">
<vn-td expand>
<span>
<span
vn-click-stop="itemDescriptor.show($event, buy.id)"
class="link">
{{::buy.itemName}}
</span>
</vn-td>
@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,6 @@
{
"name": "Expedition",
"base": "Loggable",
"log": {
"model": "TicketLog",
"relation": "ticket"
},
"options": {
"mysql": {
"table": "expedition"
@ -59,4 +55,3 @@
}
}
}

View File

@ -1,12 +1,6 @@
{
"name": "Sale",
"base": "Loggable",
"log": {
"model": "TicketLog",
"relation": "ticket",
"showField": "concept",
"grabUser": true
},
"options": {
"mysql": {
"table": "sale"

View File

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

View File

@ -14,6 +14,15 @@
},
"scopeDays": {
"type": "number"
},
"pickingDelay": {
"type": "number"
},
"packagingInvoicingDated": {
"type": "date"
},
"daysForWarningClaim": {
"type": "number"
}
}
}

View File

@ -1,10 +1,6 @@
{
"name": "TicketDms",
"base": "Loggable",
"log": {
"model": "TicketLog",
"relation": "ticket"
},
"options": {
"mysql": {
"table": "ticketDms"

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