2595 - Added invoiceIn module #545

Merged
carlosjr merged 11 commits from 2595-invoiceIn-index into dev 2021-03-03 11:47:43 +00:00
72 changed files with 1886 additions and 519 deletions
Showing only changes of commit 718021cff8 - Show all commits

View File

@ -34,4 +34,5 @@ rules:
no-multiple-empty-lines: ["error", { "max": 1, "maxEOF": 1 }]
space-in-parens: ["error", "never"]
jasmine/no-focused-tests: 0
jasmine/prefer-toHaveBeenCalledWith: 0
jasmine/prefer-toHaveBeenCalledWith: 0
arrow-spacing: ["error", { "before": true, "after": true }]

View File

@ -110,11 +110,10 @@ module.exports = Self => {
async function createDms(ctx, file, myOptions) {
const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId;
const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions);
const args = ctx.args;
const newDms = await Self.create({
workerFk: myWorker.id,
workerFk: myUserId,
dmsTypeFk: args.dmsTypeId,
companyFk: args.companyId,
warehouseFk: args.warehouseId,

View File

@ -0,0 +1,13 @@
module.exports = Self => {
Self.validatesPresenceOf('name', {
message: 'Name cannot be blank'
});
Self.validatesPresenceOf('bic', {
message: 'Swift / BIC cannot be empty'
});
Self.validatesUniquenessOf('bic', {
message: 'This BIC already exist.'
});
};

View File

@ -1,11 +1,51 @@
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');
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) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
@ -48,13 +88,31 @@ module.exports = Self => {
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
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
}
};
}
const resizeOpts = {
withoutEnlargement: true,
fit: 'inside'
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
.png()
.toFile(dstFile);
@ -69,7 +127,7 @@ module.exports = Self => {
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(size.width, size.height, resizeOpts)
.png()
.toFile(dstFile);

View File

@ -1 +1,5 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('InvoiceIn', '*', '*', 'ALLOW', 'ROLE', 'administrative');
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('SupplierAccount', '*', '*', 'ALLOW', 'ROLE', 'administrative'),
('Entry', '*', '*', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', '*', '*', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1,137 @@
DROP PROCEDURE `vn`.`item_getBalance`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`item_getBalance`(IN vItemId INT, IN vWarehouse INT)
BEGIN
DECLARE vDateInventory DATETIME;
DECLARE vCurdate DATE DEFAULT CURDATE();
DECLARE vDayEnd DATETIME DEFAULT util.dayEnd(vCurdate);
SELECT inventoried INTO vDateInventory FROM config;
SET @a = 0;
SET @currentLineFk = 0;
SET @shipped = '';
SELECT DATE(@shipped:= shipped) shipped,
alertLevel,
stateName,
origin,
reference,
clientFk,
name,
`in`,
`out`,
@a := @a + IFNULL(`in`,0) - IFNULL(`out`,0) as balance,
@currentLineFk := IF (@shipped < CURDATE()
OR (@shipped = CURDATE() AND (isPicked OR alertLevel >= 2)),
lineFk,@currentLineFk) lastPreparedLineFk,
isTicket,
lineFk,
isPicked,
clientType
FROM
( SELECT tr.landed AS shipped,
b.quantity AS `in`,
NULL AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
s.name AS name,
e.ref AS reference,
e.id AS origin,
s.id AS clientFk,
IF(al.alertLevel = 3, TRUE, FALSE) isPicked,
FALSE AS isTicket,
b.id lineFk,
NULL `order`,
NULL AS clientType
FROM buy b
JOIN entry e ON e.id = b.entryFk
JOIN travel tr ON tr.id = e.travelFk
JOIN supplier s ON s.id = e.supplierFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN tr.shipped < CURDATE() THEN 3
WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
ELSE 0
END
JOIN state st ON st.code = al.code
WHERE tr.landed >= vDateInventory
AND vWarehouse = tr.warehouseInFk
AND b.itemFk = vItemId
AND e.isInventory = FALSE
AND e.isRaid = FALSE
UNION ALL
SELECT tr.shipped,
NULL as `in`,
b.quantity AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
s.name AS name,
e.ref AS reference,
e.id AS origin,
s.id AS clientFk,
IF(al.alertLevel = 3, TRUE, FALSE) isPicked,
FALSE AS isTicket,
b.id,
NULL `order`,
NULL AS clientType
FROM buy b
JOIN entry e ON e.id = b.entryFk
JOIN travel tr ON tr.id = e.travelFk
JOIN warehouse w ON w.id = tr.warehouseOutFk
JOIN supplier s ON s.id = e.supplierFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN tr.shipped < CURDATE() THEN 3
WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
ELSE 0
END
JOIN state st ON st.code = al.code
WHERE tr.shipped >= vDateInventory
AND vWarehouse =tr.warehouseOutFk
AND s.id <> 4
AND b.itemFk = vItemId
AND e.isInventory = FALSE
AND w.isFeedStock = FALSE
AND e.isRaid = FALSE
UNION ALL
SELECT DATE(t.shipped),
NULL as `in`,
s.quantity AS `out`,
al.alertLevel AS alertLevel,
st.name AS stateName,
t.nickname AS name,
t.refFk AS reference,
t.id AS origin,
t.clientFk,
stk.id AS isPicked,
TRUE AS isTicket,
s.id,
st.`order`,
ct.code AS clientType
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
LEFT JOIN ticketState ts ON ts.ticket = t.id
LEFT JOIN state st ON st.code = ts.code
JOIN client c ON c.id = t.clientFk
JOIN clientType ct ON ct.id = c.clientTypeFk
JOIN alertLevel al ON al.alertLevel =
CASE
WHEN t.shipped < curdate() THEN 3
WHEN t.shipped > util.dayEnd(curdate()) THEN 0
ELSE IFNULL(ts.alertLevel, 0)
END
LEFT JOIN state stPrep ON stPrep.`code` = 'PREPARED'
LEFT JOIN saleTracking stk ON stk.saleFk = s.id AND stk.stateFk = stPrep.id
WHERE t.shipped >= vDateInventory
AND s.itemFk = vItemId
AND vWarehouse =t.warehouseFk
ORDER BY shipped, alertLevel DESC, isTicket, `order` DESC, isPicked DESC, `in` DESC, `out` DESC
) AS itemDiary;
END$$
DELIMITER ;

View File

@ -150,20 +150,20 @@ INSERT INTO `vn`.`shelving` (`code`, `parkingFk`, `isPrinted`, `priority`, `park
INSERT INTO `vn`.`accountingType`(`id`, `description`, `receiptDescription`,`code`)
VALUES
(1, 'CC y Polizas de crédito', NULL, NULL),
(2, 'Cash', NULL, 'cash'),
(3, 'Credit card', NULL, 'creditCard'),
(4, 'Finalcial lines', NULL, NULL),
(5, 'Other products', NULL, NULL),
(6, 'Loans', NULL, NULL),
(7, 'Leasing', NULL, NULL),
(8, 'Compensations', 'Compensations', 'Compensations');
(1, 'CC y Polizas de crédito', NULL, NULL),
(2, 'Cash', 'Cash', 'cash'),
(3, 'Credit card', 'Credit Card', 'creditCard'),
(4, 'Finalcial lines', NULL, NULL),
(5, 'Other products', NULL, NULL),
(6, 'Loans', NULL, NULL),
(7, 'Leasing', NULL, NULL),
(8, 'Compensations', 'Compensations', 'compensation');
INSERT INTO `vn`.`bank`(`id`, `bank`, `account`, `cash`, `entityFk`, `isActive`, `currencyFk`)
VALUES
(1, 'Pay on receipt', '0000000000', 3, 0, 1, 1),
(2, 'Cash', '1111111111', 2, 0, 1, 1),
(3, 'Compensation', '0000000000', 8, 0, 1, 1);
(1, 'Pay on receipt', '5720000001', 3, 0, 1, 1),
(2, 'Cash', '5700000001', 2, 0, 1, 1),
(3, 'Compensation', '4000000000', 8, 0, 1, 1);
INSERT INTO `vn`.`deliveryMethod`(`id`, `code`, `description`)
VALUES

View File

@ -316,7 +316,7 @@ export default {
fourthRelevancy: 'vn-item-tags vn-horizontal:nth-child(4) [ng-model="itemTag.priority"]',
fourthRemoveTagButton: 'vn-item-tags vn-horizontal:nth-child(4) vn-icon-button[icon="delete"]',
fifthTag: 'vn-item-tags vn-horizontal:nth-child(5) > vn-autocomplete[ng-model="itemTag.tagFk"]',
fifthValue: 'vn-item-tags vn-horizontal:nth-child(5) vn-textfield[ng-model="itemTag.value"]',
fifthValue: 'vn-item-tags vn-horizontal:nth-child(5) vn-autocomplete[ng-model="itemTag.value"]',
fifthRelevancy: 'vn-item-tags vn-horizontal:nth-child(5) vn-input-number[ng-model="itemTag.priority"]',
sixthTag: 'vn-item-tags vn-horizontal:nth-child(6) > vn-autocomplete[ng-model="itemTag.tagFk"]',
sixthValue: 'vn-item-tags vn-horizontal:nth-child(6) vn-textfield[ng-model="itemTag.value"]',

View File

@ -47,6 +47,7 @@ describe('Client balance path', () => {
await page.closePopup();
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Cash');
await page.clearInput(selectors.clientBalance.newDescription);
await page.write(selectors.clientBalance.newDescription, 'Description');
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
@ -86,6 +87,7 @@ describe('Client balance path', () => {
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.write(selectors.clientBalance.newPaymentAmount, amountPaid);
await page.clearInput(selectors.clientBalance.newDescription);
await page.write(selectors.clientBalance.newDescription, 'Payment');
await page.write(selectors.clientBalance.deliveredAmount, cashHanded);
const refund = await page.waitToGetProperty(selectors.clientBalance.refundAmount, 'value');
@ -107,6 +109,7 @@ describe('Client balance path', () => {
await page.waitToClick(selectors.clientBalance.newPaymentButton);
await page.autocompleteSearch(selectors.clientBalance.newPaymentBank, 'Pay on receipt');
await page.overwrite(selectors.clientBalance.newPaymentAmount, '-150');
await page.clearInput(selectors.clientBalance.newDescription);
await page.write(selectors.clientBalance.newDescription, 'Description');
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();

View File

@ -16,7 +16,7 @@ describe('Item create tags path', () => {
await browser.close();
});
it(`should create a new tag and delete a former one`, async() => {
it('should create a new tag and delete a former one', async() => {
await page.waitToClick(selectors.itemTags.fourthRemoveTagButton);
await page.waitToClick(selectors.itemTags.addItemTagButton);
await page.autocompleteSearch(selectors.itemTags.seventhTag, 'Ancho de la base');
@ -29,7 +29,7 @@ describe('Item create tags path', () => {
expect(message.text).toContain('Data saved!');
});
it(`should confirm the fourth row data is the expected one`, async() => {
it('should confirm the fourth row data is the expected one', async() => {
await page.reloadSection('item.card.tags');
await page.waitForSelector('vn-item-tags');
let result = await page.waitToGetProperty(selectors.itemTags.fourthTag, 'value');
@ -47,7 +47,7 @@ describe('Item create tags path', () => {
expect(result).toEqual('4');
});
it(`should confirm the fifth row data is the expected one`, async() => {
it('should confirm the fifth row data is the expected one', async() => {
let tag = await page
.waitToGetProperty(selectors.itemTags.fifthTag, 'value');
@ -62,7 +62,7 @@ describe('Item create tags path', () => {
expect(relevancy).toEqual('5');
});
it(`should confirm the sixth row data is the expected one`, async() => {
it('should confirm the sixth row data is the expected one', async() => {
let tag = await page
.waitToGetProperty(selectors.itemTags.sixthTag, 'value');

View File

@ -174,4 +174,9 @@ vn-table {
.vn-check {
margin: 0;
}
.empty-rows {
color: $color-font-secondary;
font-size: 1.375rem;
text-align: center;
}
}

View File

@ -0,0 +1,40 @@
<vn-dialog class="edit"
vn-id="bankEntityDialog"
on-open="$ctrl.resetData()"
on-accept="$ctrl.onAccept()"
message="New bank entity">
<tpl-body>
<p translate>Please, ensure you put the correct data!</p>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
vn-id="entityName"
label="Name"
ng-model="$ctrl.data.name"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
vn-id="bic"
label="Swift"
ng-model="$ctrl.data.bic"
required="true">
</vn-textfield>
<vn-autocomplete vn-one
ng-model="$ctrl.data.countryFk"
url="Countries"
show-field="country"
value-field="id"
label="Country">
</vn-autocomplete>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button id= "saveBankEntity" response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,37 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller extends Component {
open() {
this.$.bankEntityDialog.show();
}
resetData() {
this.data = {};
}
onAccept() {
try {
if (!this.data.countryFk)
throw new Error(`The country can't be empty`);
this.$http.post(`bankEntities`, this.data).then(res => {
this.vnApp.showMessage(this.$t('The bank entity has been created. You can save the data now'));
this.emit('response', {$response: res.data});
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
}
ngModule.vnComponent('vnNewBankEntity', {
template: require('./index.html'),
controller: Controller,
bindings: {
data: '<',
}
});

View File

@ -0,0 +1,53 @@
import './index';
describe('Salix Component vnNewBankEntity', () => {
let controller;
let $httpBackend;
let $scope;
let $element;
let vnApp;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_) => {
$httpBackend = _$httpBackend_;
vnApp = _vnApp_;
jest.spyOn(vnApp, 'showError');
$scope = $rootScope.$new();
$element = angular.element('<vn-dialog></dialog>');
controller = $componentController('vnNewBankEntity', {$element, $scope});
}));
describe('resetData()', () => {
it('should reset the location in the controller', () => {
expect(controller.data).toBeUndefined();
controller.resetData();
expect(controller.data).toEqual({});
});
});
describe('onAccept()', () => {
it('should throw an error if there is no country id in the location', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.data = {};
controller.onAccept();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The country can't be empty`);
});
it('should do add the new bank entity', () => {
controller.data = {
countryFk: 1
};
$httpBackend.expectPOST('bankEntities', controller.data).respond(200, controller.data);
controller.onAccept();
$httpBackend.flush();
});
});
});

View File

@ -0,0 +1,12 @@
New postcode: Nuevo código postal
New city: Nueva ciudad
New province: Nueva provincia
Please, ensure you put the correct data!: ¡Por favor, asegúrate de poner los datos correctos!
The postcode can't be empty: El código postal no puede quedar vacío
The town can't be empty: La población no puede quedar vacía
The province can't be empty: La provincia no puede quedar vacía
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
The bank entity has been created. You can save the data now: Se ha creado la entidad bancaria. Puedes guardar los datos ahora

View File

@ -0,0 +1,9 @@
@import "variables";
vn-new-bank-entity {
vn-dialog {
p {
color: $color-alert
}
}
}

View File

@ -14,3 +14,4 @@ import './summary';
import './topbar/topbar';
import './user-popover';
import './upload-photo';
import './bank-entity';

View File

@ -91,5 +91,6 @@
"The observation type can't be repeated": "The observation type can't be repeated",
"New ticket request has been created with price": "New ticket request has been created *'{{description}}'* for day *{{shipped}}*, with a quantity of *{{quantity}}* and a price of *{{price}} €*",
"New ticket request has been created": "New ticket request has been created *'{{description}}'* for day *{{shipped}}*, with a quantity of *{{quantity}}*",
"There's a new urgent ticket": "There's a new urgent ticket: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})"
"There's a new urgent ticket": "There's a new urgent ticket: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})",
"Swift / BIC cannot be empty": "Swift / BIC cannot be empty"
}

View File

@ -167,9 +167,12 @@
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera",
"Invalid account": "Cuenta inválida",
"New ticket request has been created with price": "Se ha creado una nueva petición de compra *'{{description}}'* para el día *{{shipped}}*, con una cantidad de *{{quantity}}* y un precio de *{{price}} €*",
"New ticket request has been created": "Se ha creado una nueva petición de compra *'{{description}}'* para el día *{{shipped}}*, con una cantidad de *{{quantity}}*",
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong> y un precio de <strong>{{price}} €</strong>",
"New ticket request has been created": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong>",
"Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío",
"This BIC already exist.": "Este BIC ya existe.",
"That item doesn't exists": "Ese artículo no existe",
"There's a new urgent ticket": "Hay un nuevo ticket urgente: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})"
"There's a new urgent ticket": "Hay un nuevo ticket urgente: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})",
"Invalid account": "Cuenta inválida",
"Compensation account is empty": "La cuenta para compensar está vacia"
}

View File

@ -62,7 +62,10 @@ module.exports = function(Self) {
const bank = await models.Bank.findById(args.bankFk);
const accountingType = await models.AccountingType.findById(bank.accountingTypeFk);
if (args.compensationAccount) {
if (accountingType.code == 'compensation') {
if (!args.compensationAccount)
throw new UserError('Compensation account is empty');
const supplierCompensation = await models.Supplier.findOne({
where: {
account: args.compensationAccount
@ -92,12 +95,11 @@ module.exports = function(Self) {
],
options);
} else {
const description = `${clientOriginal.id} : ${clientOriginal.nickname} - ${accountingType.receiptDescription}`;
const description = `${clientOriginal.id} : ${clientOriginal.socialName} - ${accountingType.receiptDescription}`;
const [xdiarioNew] = await Self.rawSql(
`SELECT xdiario_new(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ledger;`,
`SELECT xdiario_new(?, CURDATE(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ledger;`,
[
null,
Date(),
bank.account,
clientOriginal.accountingAccount,
description,
@ -114,10 +116,9 @@ module.exports = function(Self) {
options);
await Self.rawSql(
`SELECT xdiario_new(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
`SELECT xdiario_new(?, CURDATE(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
[
xdiarioNew.ledger,
Date(),
clientOriginal.accountingAccount,
bank.account,
description,

View File

@ -26,15 +26,25 @@ module.exports = Self => {
Self.lastActiveTickets = async(id, ticketId) => {
const ticket = await Self.app.models.Ticket.findById(ticketId);
const query = `
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName, ad.city AS address
FROM vn.ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id
JOIN vn.address ad ON t.addressFk = ad.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0
AND t.id <> ? AND t.warehouseFk = ?
ORDER BY t.shipped
SELECT
t.id,
t.shipped,
a.name AS agencyName,
w.name AS warehouseName,
ad.nickname AS nickname,
ad.city AS city,
ad.postalCode AS postalCode,
ad.street AS street,
pr.name AS name
FROM ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id
JOIN vn.address ad ON t.addressFk = ad.id
JOIN vn.province pr ON ad.provinceFk = pr.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0
AND t.id <> ? AND t.warehouseFk = ?
ORDER BY t.shipped
LIMIT 10`;
return Self.rawSql(query, [id, ticketId, ticket.warehouseFk]);

View File

@ -1,4 +1,5 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('Client createReceipt', () => {
const clientFk = 108;
@ -6,18 +7,34 @@ describe('Client createReceipt', () => {
const companyFk = 442;
const amountPaid = 12.50;
const description = 'Receipt description';
const activeCtx = {
accessToken: {userId: 5},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
activeCtx.http.req.__ = value => {
return value;
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new receipt', async() => {
const bankFk = 1;
let ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description
}
ctx.args = {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description
};
const receipt = await app.models.Client.createReceipt(ctx);
@ -38,19 +55,39 @@ describe('Client createReceipt', () => {
await till.destroy();
});
it('should throw Compensation account is empty', async() => {
const bankFk = 3;
ctx.args = {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description
};
try {
await app.models.Client.createReceipt(ctx);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual('Compensation account is empty');
});
it('should throw Invalid account if compensationAccount does not belongs to a client nor a supplier', async() => {
let error;
const bankFk = 3;
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: 'non existing account'
}
ctx.args = {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: 'non existing account'
};
try {
@ -65,16 +102,15 @@ describe('Client createReceipt', () => {
it('should create a new receipt with a compensation for a client', async() => {
const bankFk = 3;
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: '4300000001'
}
ctx.args = {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: '4300000001'
};
const receipt = await app.models.Client.createReceipt(ctx);
const receiptCompensated = await app.models.Receipt.findOne({
@ -104,16 +140,16 @@ describe('Client createReceipt', () => {
});
it('should create a new receipt with a compensation for a supplier', async() => {
const ctx = {
args: {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: 3,
amountPaid: amountPaid,
description: description,
compensationAccount: '4100000001'
}
const bankFk = 3;
ctx.args = {
clientFk: clientFk,
payed: payed,
companyFk: companyFk,
bankFk: bankFk,
amountPaid: amountPaid,
description: description,
compensationAccount: '4100000001'
};
const receipt = await app.models.Client.createReceipt(ctx);

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('Client last active tickets', () => {
it('should receive an array of last active tickets of Bruce Wayne', async() => {
const ticketId = 22;
const clientId = 109;
const warehouseId = 5;
const result = await app.models.Client.lastActiveTickets(clientId, ticketId, warehouseId);
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
const properties = Object.keys(anyResult);
expect(properties.length).toEqual(9);
expect(result.length).toEqual(3);
});
});

View File

@ -1,8 +0,0 @@
module.exports = Self => {
Self.validatesPresenceOf('name', {
message: `Name cannot be blank`
});
Self.validatesPresenceOf('bic', {
message: `Swift / BIC can't be empty`
});
};

View File

@ -1,3 +1,5 @@
const LoopBackContext = require('loopback-context');
module.exports = function(Self) {
require('../methods/receipt/filter')(Self);
@ -23,13 +25,10 @@ module.exports = function(Self) {
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) {
let token = ctx.options.accessToken;
let userId = token && token.userId;
ctx.instance.workerFk = userId;
const loopBackContext = LoopBackContext.getCurrentContext();
ctx.instance.workerFk = loopBackContext.active.accessToken.userId;
await Self.app.models.Till.create({
workerFk: userId,
workerFk: ctx.instance.workerFk,
bankFk: ctx.instance.bankFk,
in: ctx.instance.amountPaid,
concept: ctx.instance.description,

View File

@ -78,6 +78,8 @@ module.exports = Self => {
return {'ic.id': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'code':
return {'it.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'active':
@ -103,6 +105,7 @@ module.exports = Self => {
i.id AS itemFk,
i.size,
i.density,
it.code,
i.typeFk,
i.family,
i.isActive,

View File

@ -37,8 +37,8 @@
<vn-th field="quantity">Quantity</vn-th>
<vn-th field="description" style="text-align: center">Description</vn-th>
<vn-th field="size">Size</vn-th>
<vn-th field="tags" style="text-align: center">Tags</vn-th>
<vn-th field="type">Type</vn-th>
<vn-th field="name" style="text-align: center">Tags</vn-th>
<vn-th field="code">Type</vn-th>
<vn-th field="intrastat">Intrastat</vn-th>
<vn-th field="origin">Origin</vn-th>
<vn-th field="density">Density</vn-th>
@ -109,7 +109,7 @@
</vn-fetched-tags>
</vn-td>
<vn-td shrink title="{{::buy.type}}">
{{::buy.type}}
{{::buy.code}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::buy.intrastat}}

View File

@ -31,19 +31,19 @@
"state": "entry.index",
"component": "vn-entry-index",
"description": "Entries",
"acl": ["buyer"]
"acl": ["buyer", "administrative"]
}, {
"url": "/latest-buys?q",
"state": "entry.latestBuys",
"component": "vn-entry-latest-buys",
"description": "Latest buys",
"acl": ["buyer"]
"acl": ["buyer", "administrative"]
}, {
"url": "/create?supplierFk&travelFk&companyFk",
"state": "entry.create",
"component": "vn-entry-create",
"description": "New entry",
"acl": ["buyer"]
"acl": ["buyer", "administrative"]
}, {
"url": "/:id",
"state": "entry.card",

View File

@ -41,18 +41,13 @@ module.exports = Self => {
async function download() {
const image = await Self.findOne({
where: {url: {neq: null}, attempts: {lt: maxAttempts}},
order: 'attempts, updated'
order: 'priority, attempts, updated'
});
if (!image) return;
const srcFile = image.url.split('/').pop();
const dotIndex = srcFile.lastIndexOf('.');
let fileName = srcFile.substring(0, dotIndex);
if (dotIndex == -1)
fileName = srcFile;
const srcFile = image.url;
const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, '');
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);

View File

@ -44,6 +44,10 @@ module.exports = Self => {
arg: 'description',
type: 'String',
description: 'The item description',
}, {
arg: 'stemMultiplier',
type: 'Integer',
description: 'The item multiplier',
}
],
returns: {
@ -80,16 +84,22 @@ module.exports = Self => {
: {or: [{'i.name': {like: `%${value}%`}}, codeWhere]};
case 'id':
return {'i.id': value};
case 'description':
return {'i.description': {like: `%${value}%`}};
case 'categoryFk':
return {'ic.id': value};
case 'salesPersonFk':
return {'t.workerFk': value};
case 'typeFk':
return {'i.typeFk': value};
case 'isActive':
return {'i.isActive': value};
case 'multiplier':
return {'i.stemMultiplier': value};
case 'typeFk':
return {'i.typeFk': value};
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'intrastat':
return {'intr.description': value};
}
});
filter = mergeFilters(filter, {where});
@ -98,7 +108,8 @@ module.exports = Self => {
let stmt;
stmt = new ParameterizedSQL(
`SELECT i.id,
`SELECT
i.id,
i.image,
i.name,
i.description,
@ -111,29 +122,30 @@ module.exports = Self => {
i.tag10, i.value10,
i.subName,
i.isActive,
t.name type,
t.workerFk buyerFk,
u.name userName,
intr.description AS intrastat,
i.stems,
ori.code AS origin,
ic.name AS category,
i.density,
i.stemMultiplier,
i.typeFk,
it.name AS typeName,
it.workerFk AS buyerFk,
u.name AS userName,
ori.code AS origin,
ic.name AS category,
intr.description AS intrastat,
b.grouping,
b.packing,
itn.code AS niche, @visibleCalc
ip.code AS niche, @visibleCalc
FROM item i
LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = t.categoryFk
LEFT JOIN worker w ON w.id = t.workerFk
LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN worker w ON w.id = it.workerFk
LEFT JOIN account.user u ON u.id = w.userFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN producer pr ON pr.id = i.producerFk
LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = t.warehouseFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = it.warehouseFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
LEFT JOIN itemPlacement itn ON itn.itemFk = i.id AND itn.warehouseFk = t.warehouseFk`
LEFT JOIN itemPlacement ip ON ip.itemFk = i.id AND ip.warehouseFk = it.warehouseFk`
);
if (ctx.args.tags) {

View File

@ -20,8 +20,12 @@ module.exports = Self => {
});
Self.getBalance = async filter => {
let where = filter.where;
const where = filter.where;
let [diary] = await Self.rawSql(`CALL vn.item_getBalance(?, ?)`, [where.itemFk, where.warehouseFk]);
for (const entry of diary)
if (entry.clientType === 'loses') entry.highlighted = true;
return diary;
};
};

View File

@ -0,0 +1,33 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('item getBalance()', () => {
it('should return the balance lines of a client type loses in which one has highlighted true', async() => {
const activeCtx = {
accessToken: {userId: 9},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const losesClientId = 111;
const ticket = await app.models.Ticket.findById(7);
const originalClientId = ticket.clientFk;
await ticket.updateAttribute('clientFk', losesClientId);
const filter = {
where: {
itemFk: 1,
warehouseFk: 1
}
};
const results = await app.models.Item.getBalance(filter);
const result = results.find(element => element.clientType == 'loses');
expect(result.highlighted).toBe(true);
// restores
await ticket.updateAttribute('clientFk', originalClientId);
});
});

View File

@ -113,6 +113,12 @@
ng-model="$ctrl.item.stems"
rule>
</vn-input-number>
<vn-input-number
vn-one
min="0"
label="Multiplier"
ng-model="$ctrl.item.stemMultiplier">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number

View File

@ -9,4 +9,5 @@ Price in kg: Precio en kg
New intrastat: Nuevo intrastat
Identifier: Identificador
Fragile: Frágil
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Multiplier: Multiplicador

View File

@ -64,14 +64,16 @@
<vn-td>{{::sale.stateName | dashIfEmpty}}</vn-td>
<vn-td>{{::sale.reference | dashIfEmpty}}</vn-td>
<vn-td class="truncate" expand>
<span ng-if="::!sale.isTicket">
{{::sale.name | dashIfEmpty}}
</span>
<span
ng-if="::sale.isTicket"
vn-click-stop="clientDescriptor.show($event, sale.clientFk)"
class="link">
{{::sale.name | dashIfEmpty}}
<span ng-class="::{'warning chip': sale.highlighted}">
<span ng-if="::!sale.isTicket">
{{::sale.name | dashIfEmpty}}
</span>
<span
ng-if="::sale.isTicket"
vn-click-stop="clientDescriptor.show($event, sale.clientFk)"
class="link">
{{::sale.name | dashIfEmpty}}
</span>
</span>
</vn-td>
<vn-td number class="in">{{::sale.in | dashIfEmpty}}</vn-td>

View File

@ -5,107 +5,107 @@
model="model"
class="vn-w-xl vn-mb-xl">
<vn-card>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="type" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.type}}">
{{::item.type}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="name">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="typeFk" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.typeName}}">
{{::item.typeName}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
@ -127,4 +127,31 @@
<vn-item-summary
item="$ctrl.itemSelected">
</vn-item-summary>
</vn-popup>
</vn-popup>
<vn-contextmenu
vn-id="contextmenu"
targets="['vn-data-viewer']"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -11,6 +11,36 @@ class Controller extends Section {
};
}
exprBuilder(param, value) {
switch (param) {
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'grouping':
return {'b.grouping': value};
case 'packing':
return {'b.packing': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'intrastat':
return {'intr.description': value};
case 'id':
case 'size':
case 'name':
case 'subname':
case 'isActive':
case 'density':
case 'stemMultiplier':
case 'stems':
return {[`i.${param}`]: value};
}
}
onCloneAccept(itemFk) {
return this.$http.post(`Items/${itemFk}/clone`)
.then(res => {

View File

@ -1,6 +1,6 @@
<div class="search-panel">
<form class="vn-pa-lg" ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
@ -8,7 +8,7 @@
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Ticket id"
@ -25,7 +25,7 @@
<tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Client id"
@ -38,19 +38,37 @@
url="Warehouses">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker
vn-one
label="From"
ng-model="filter.from">
ng-model="filter.from"
on-change="$ctrl.from = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="filter.to">
ng-model="filter.to"
on-change="$ctrl.to = value">
</vn-date-picker>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</vn-input-number>
<vn-icon color-marginal
icon="info"
vn-tooltip="Cannot choose a range of dates and days onward at the same time">
</vn-icon>
</vn-horizontal>
<vn-horizontal>
</section>
<vn-horizontal class="vn-px-lg">
<vn-check vn-one
triple-state="true"
label="For me"
@ -65,7 +83,7 @@
<tpl-item>{{name}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-mt-lg">
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>

View File

@ -11,6 +11,35 @@ class Controller extends SearchPanel {
{code: 'denied', name: this.$t('Denied')}
];
}
get from() {
return this._from;
}
set from(value) {
this._from = value;
this.filter.scopeDays = null;
}
get to() {
return this._to;
}
set to(value) {
this._to = value;
this.filter.scopeDays = null;
}
get scopeDays() {
return this._scopeDays;
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.from = null;
this.filter.to = null;
}
}
ngModule.vnComponent('vnRequestSearchPanel', {

View File

@ -0,0 +1,48 @@
import './index';
describe(' Component vnRequestSearchPanel', () => {
let controller;
beforeEach(ngModule('item'));
beforeEach(inject($componentController => {
controller = $componentController('vnRequestSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('from() setter', () => {
it('should clear the scope days when setting the from property', () => {
controller.filter.scopeDays = 1;
controller.from = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.from).toBeDefined();
});
});
describe('to() setter', () => {
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.to = new Date();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.to).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.from = new Date();
controller.filter.to = new Date();
controller.scopeDays = 1;
expect(controller.filter.from).toBeNull();
expect(controller.filter.to).toBeNull();
expect(controller.scopeDays).toBeDefined();
});
});
});

View File

@ -10,9 +10,10 @@
<vn-portal slot="topbar">
<vn-searchbar
panel="vn-request-search-panel"
suggested-filter="$ctrl.filterParams"
info="Search request by id or alias"
suggested-filter="$ctrl.filterParams"
filter="$ctrl.filterParams"
fetch-params="$ctrl.fetchParams($params)"
model="model"
auto-state="false">
</vn-searchbar>

View File

@ -15,7 +15,6 @@ export default class Controller extends Section {
nextWeek.setDate(nextWeek.getDate() + 7);
this.filterParams = {
mine: true,
from: today,
to: nextWeek,
state: 'pending'
@ -23,6 +22,24 @@ export default class Controller extends Section {
}
}
fetchParams($params) {
if (!Object.entries($params).length)
$params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') {
const from = new Date();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + $params.scopeDays);
to.setHours(23, 59, 59, 999);
Object.assign($params, {from, to});
}
return $params;
}
getState(isOk) {
if (isOk === null)
return 'Pending';

View File

@ -162,7 +162,7 @@
"acl": ["buyer"]
},
{
"url" : "/fixed-price",
"url" : "/fixed-price?q",
"state": "item.fixedPrice",
"component": "vn-fixed-price",
"description": "Fixed prices",

View File

@ -55,6 +55,9 @@
<vn-label-value label="stems"
value="{{$ctrl.summary.item.stems}}">
</vn-label-value>
<vn-label-value label="Multiplier"
value="{{$ctrl.summary.item.stemMultiplier}}">
</vn-label-value>
<vn-label-value label="Buyer">
<span
ng-click="workerDescriptor.show($event, $ctrl.summary.item.itemType.worker.userFk)"

View File

@ -32,14 +32,14 @@
rule>
</vn-autocomplete>
<vn-textfield vn-three
ng-show="tag.selection.isFree || tag.selection.isFree == undefined"
ng-if="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"
ng-if="tag.selection.isFree === false"
url="{{'Tags/' + itemTag.tagFk + '/filterValue'}}"
search-function="{value: $search}"
label="Value"

View File

@ -37,7 +37,7 @@ class Controller extends Component {
};
filter = encodeURIComponent(JSON.stringify(filter));
let query = `Clients?filter=${filter}`;
this.$http.get(query).then(res => {
this.$http.get(query).then(res=> {
if (res.data) {
let client = res.data[0];
let defaultAddress = client.defaultAddress;

View File

@ -1,6 +1,10 @@
{
"name": "SupplierAccount",
"base": "VnModel",
"base": "Loggable",
"log": {
"model":"SupplierLog",
"relation": "supplier"
},
"options": {
"mysql": {
"table": "supplierAccount"
@ -45,6 +49,11 @@
"type": "belongsTo",
"model": "Supplier",
"foreignKey": "supplierFk"
},
"bankEntity": {
"type": "belongsTo",
"model": "BankEntity",
"foreignKey": "bankEntityFk"
}
}
}

View File

@ -0,0 +1,67 @@
<vn-crud-model
vn-id="model"
url="SupplierAccounts"
fields="['id', 'supplierFk', 'iban', 'bankEntityFk']"
link="{supplierFk: $ctrl.$params.id}"
include="$ctrl.include"
data="$ctrl.supplierAccounts"
auto-load="true">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.supplierAccounts"
form="form">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal ng-repeat="supplierAccount in $ctrl.supplierAccounts">
<vn-textfield vn-three
ng-show="supplierAccount.iban || supplierAccount.iban == undefined"
label="Iban"
ng-model="supplierAccount.iban"
rule>
</vn-textfield>
<vn-autocomplete vn-two
label="Bank entity"
ng-model="supplierAccount.bankEntityFk"
url="BankEntities"
show-field="name"
rule>
</vn-autocomplete>
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New bank entity"
ng-click="$ctrl.showBankEntity($event, $index)">
</vn-icon-button>
</append>
<vn-none>
<vn-icon-button
vn-tooltip="Remove account"
icon="delete"
ng-click="model.remove($index)"
tabindex="-1">
</vn-icon-button>
</vn-none>
</vn-horizontal>
<vn-one>
<vn-icon-button
vn-bind="+"
vn-tooltip="Add account"
icon="add_circle"
ng-click="$ctrl.add()">
</vn-icon-button>
</vn-one>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
</vn-button-bar>
</form>
<!-- New bankentity dialog -->
<vn-new-bank-entity
vn-id="bankEntity"
on-response="$ctrl.onResponse($response)">
</vn-new-bank-entity>

View File

@ -0,0 +1,56 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $) {
super($element, $);
this.include = {
relation: 'bankEntity',
scope: {
fields: ['countryFk', 'id', 'name', 'bic']
}
};
}
add() {
this.$.model.insert({
supplierFk: this.$params.id
});
}
onResponse(response) {
const data = this.$.model.data;
const supplierAccount = data[this.currentRowIndex];
supplierAccount.bankEntityFk = response.id;
}
showBankEntity(event, $index) {
if (event.defaultPrevented) return;
event.preventDefault();
this.currentRowIndex = $index;
this.$.bankEntity.open();
}
onBankEntityAccept() {
const query = `SupplierAccounts/${this.$params.id}/createBankEntity`;
return this.$http.patch(query, this.newBankEntity)
.then(res => this.supplierAccount.bankEntityFk = res.data.id);
}
onSubmit() {
this.$.watcher.check();
this.$.model.save().then(() => {
this.$.watcher.notifySaved();
this.$.watcher.updateOriginalData();
this.card.reload();
});
}
}
ngModule.vnComponent('vnSupplierAccount', {
template: require('./index.html'),
controller: Controller,
require: {
card: '^vnSupplierCard'
}
});

View File

@ -0,0 +1,71 @@
import './index.js';
describe('Supplier Component vnSupplierAccount', () => {
let $scope;
let $element;
let controller;
let $httpBackend;
beforeEach(ngModule('supplier'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.bankEntity = {
open: () => {}
};
$element = angular.element('<vn-supplier-accounts></supplier-accounts>');
controller = $componentController('vnSupplierAccount', {$element, $scope});
controller.supplierAccount = {
supplierFk: 442,
name: 'Verdnatura'
};
}));
describe('showBankEntity()', () => {
it('should do nothing if it default is prevented', () => {
const event = {
defaultPrevented: true,
preventDefault: () => {}
};
jest.spyOn(event, 'preventDefault');
jest.spyOn(controller.$.bankEntity, 'open');
controller.showBankEntity(event);
expect(event.preventDefault).not.toHaveBeenCalledWith();
expect(controller.$.bankEntity.open).not.toHaveBeenCalledWith();
});
it('should call preventDefault() and open() when the default is not prevented', () => {
const event = {
defaultPrevented: false,
preventDefault: () => {}
};
jest.spyOn(event, 'preventDefault');
jest.spyOn(controller.$.bankEntity, 'open');
controller.showBankEntity(event);
expect(event.preventDefault).toHaveBeenCalledWith();
expect(controller.$.bankEntity.open).toHaveBeenCalledWith();
});
it('should request to create a new bank entity', () => {
controller.bankEntity = {
name: 'My new bank entity',
bic: 'ES1234',
countryFk: 1,
id: 2200
};
const query = `SupplierAccounts/${controller.$.bankEntity.id}/createBankEntity`;
$httpBackend.expectPATCH(query).respond({id: 2200});
controller.onBankEntityAccept();
$httpBackend.flush();
expect(controller.supplierAccount.bankEntityFk).toEqual(controller.bankEntity.id);
});
});
});

View File

@ -0,0 +1,3 @@
Bank entity: Entidad bancaria
swift: Swift BIC
Add account: Añadir cuenta

View File

@ -9,6 +9,7 @@ import './search-panel';
import './summary';
import './basic-data';
import './fiscal-data';
import './account';
import './contact';
import './log';
import './consumption';

View File

@ -9,6 +9,7 @@
{"state": "supplier.index", "icon": "icon-supplier"}
],
"card": [
{"state": "supplier.card.account", "icon": "contact_support"},
{"state": "supplier.card.basicData", "icon": "settings"},
{"state": "supplier.card.fiscalData", "icon": "account_balance"},
{"state": "supplier.card.billingData", "icon": "icon-payment"},
@ -99,6 +100,15 @@
"supplier": "$ctrl.supplier"
},
"acl": ["administrative"]
},{
"url": "/account",
"state": "supplier.card.account",
"component": "vn-supplier-account",
"description": "Account",
"params": {
"supplier": "$ctrl.supplier"
},
"acl": ["administrative"]
}
]
}

View File

@ -101,7 +101,7 @@ module.exports = Self => {
if (!shipped && landed) {
const shippedResult = await models.Agency.getShipped(landed,
address.id, agencyModeId, warehouseId);
shipped = shippedResult && shippedResult.shipped;
shipped = (shippedResult && shippedResult.shipped) || landed;
}
if (shipped && !landed) {

View File

@ -2,12 +2,13 @@ const app = require('vn-loopback/server/server');
let UserError = require('vn-loopback/util/user-error');
describe('ticket new()', () => {
let ticket;
let ticketIdsToDelete = [];
let today = new Date();
let ctx = {req: {accessToken: {userId: 1}}};
afterAll(async done => {
await app.models.Ticket.destroyById(ticket.id);
for (id of ticketIdsToDelete)
await app.models.Ticket.destroyById(id);
done();
});
@ -28,7 +29,7 @@ describe('ticket new()', () => {
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId
).catch(e => {
error = e;
@ -53,7 +54,7 @@ describe('ticket new()', () => {
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId
).catch(response => {
expect(response.message).toEqual(`This address doesn't exist`);
@ -74,17 +75,44 @@ describe('ticket new()', () => {
agencyModeId: 1
};
ticket = await app.models.Ticket.new(ctx,
const ticket = await app.models.Ticket.new(ctx,
params.clientId,
params.shipped,
params.landed,
params.warehouseId,
params.companyFk,
params.companyId,
params.addressId,
params.agencyModeId);
let newestTicketIdInFixtures = 21;
ticketIdsToDelete.push(ticket.id);
expect(ticket.id).toBeGreaterThan(newestTicketIdInFixtures);
});
it('should return the set a shipped when the agency is not especified', async() => {
let params = {
clientId: 104,
landed: today,
shipped: null,
warehouseId: 2,
companyId: 442,
addressId: 4,
agencyModeId: null
};
const ticket = await app.models.Ticket.new(ctx,
params.clientId,
params.shipped,
params.landed,
params.warehouseId,
params.companyId,
params.addressId,
params.agencyModeId);
ticketIdsToDelete.push(ticket.id);
expect(ticket.shipped).toEqual(jasmine.any(Date));
});
});

View File

@ -39,7 +39,7 @@
</vn-autocomplete>
<vn-autocomplete
disabled="!$ctrl.clientId || !$ctrl.landed || !$ctrl.warehouseId"
data="$ctrl._availableAgencies"
data="$ctrl.agencies"
label="Agency"
show-field="agencyMode"
value-field="agencyModeFk"

View File

@ -100,9 +100,12 @@ class Controller extends Component {
ticket.agencyModeFk = null;
this.$http.get(`Agencies/getAgenciesWithWarehouse`, {params}).then(res => {
this._availableAgencies = res.data;
this.agencyModeId = this.defaultAddress.agencyModeFk;
this.agencies = res.data;
const defaultAgency = this.agencies.find(agency=> {
return agency.agencyModeFk == this.defaultAddress.agencyModeFk;
});
if (defaultAgency)
this.agencyModeId = defaultAgency.agencyModeFk;
});
}
}

View File

@ -116,4 +116,7 @@
</vn-button-menu>
</div>
</slot-body>
</vn-descriptor-content>
</vn-descriptor-content>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -326,31 +326,49 @@
icon="info">
</vn-icon>
</vn-horizontal>
<vn-table class="destinationTable">
<vn-thead>
<vn-tr>
<vn-th number>Id</vn-th>
<vn-th number>Shipped</vn-th>
<vn-th number>Agency</vn-th>
<vn-th number>Warehouse</vn-th>
<vn-th number>Address</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-data-viewer data="$ctrl.transfer.lastActiveTickets">
</vn-data-viewer>
<vn-tr
<table class="destinationTable vn-table">
<thead>
<tr>
<th translate shrink>Id</th>
<th translate>Shipped</th>
<th translate shrink>Agency</th>
<th translate expand>Address</th>
</tr>
</thead>
<tbody>
<tr
class="clickable"
ng-repeat="ticket in $ctrl.transfer.lastActiveTickets track by ticket.id"
ng-click="$ctrl.transferSales(ticket.id)">
<vn-td number>{{::ticket.id}}</vn-td>
<vn-td number>{{::ticket.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::ticket.agencyName}}</vn-td>
<vn-td number>{{::ticket.warehouseName}}</vn-td>
<vn-td number>{{::ticket.address}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<td shrink>{{::ticket.id}}</td>
<td>{{::ticket.shipped | date: 'dd/MM/yyyy'}}</td>
<td shrink>{{::ticket.agencyName}}</td>
<td expand>{{::ticket.address}}
<span vn-tooltip="
{{::ticket.nickname}}
{{::ticket.name}}
{{::ticket.street}}
{{::ticket.postalCode}}
{{::ticket.city}}">
{{::ticket.nickname}}
{{::ticket.name}}
{{::ticket.street}}
{{::ticket.postalCode}}
{{::ticket.city}}
</span>
</td>
</tr>
<tr>
<td
ng-if="!$ctrl.transfer.lastActiveTickets.length"
class="empty-rows"
colspan="4"
translate>
No results
</td>
</tr>
</tbody>
</table>
<form name="form">
<vn-horizontal class="vn-py-md">
<vn-input-number vn-one

View File

@ -0,0 +1,66 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('getAverageDays', {
description: 'Returns the average days duration and the two warehouses of the travel.',
accessType: 'READ',
accepts: [{
arg: 'agencyModeFk',
type: 'number',
required: true
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/getAverageDays`,
verb: 'GET'
}
});
Self.getAverageDays = async agencyModeFk => {
const conn = Self.dataSource.connector;
let stmts = [];
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.travel');
stmt = new ParameterizedSQL(`
CREATE TEMPORARY TABLE tmp.travel (
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
t.landed,
t.shipped,
t.agencyFk
FROM travel t
WHERE t.agencyFk = ? LIMIT 50)`, [agencyModeFk]);
stmts.push(stmt);
stmt = new ParameterizedSQL(`
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
(SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped )))
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY id DESC LIMIT 50) AS dayDuration
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY t.id DESC LIMIT 1`);
const avgDaysIndex = stmts.push(stmt) - 1;
stmts.push(
`DROP TEMPORARY TABLE
tmp.travel`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const [avgDays] = result[avgDaysIndex];
return avgDays;
};
};

View File

@ -8,6 +8,7 @@ module.exports = Self => {
require('../methods/travel/deleteThermograph')(Self);
require('../methods/travel/updateThermograph')(Self);
require('../methods/travel/extraCommunityFilter')(Self);
require('../methods/travel/getAverageDays')(Self);
require('../methods/travel/cloneWithEntries')(Self);
Self.rewriteDbError(function(err) {

View File

@ -20,6 +20,7 @@
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
on-change="$ctrl.onShippedChange(value)"
label="Shipped"
ng-model="$ctrl.travel.shipped">
</vn-date-picker>

View File

@ -7,6 +7,34 @@ class Controller extends Section {
this.travel = JSON.parse(this.$params.q);
}
onShippedChange(value) {
let hasFilledProperties;
let hasAgencyMode;
if (this.travel) {
hasAgencyMode = Boolean(this.travel.agencyModeFk);
hasFilledProperties = this.travel.landed || this.travel.warehouseInFk || this.travel.warehouseOutFk;
}
if (!hasAgencyMode || hasFilledProperties)
return;
const query = `travels/getAverageDays`;
const params = {
agencyModeFk: this.travel.agencyModeFk
};
this.$http.get(query, {params}).then(res => {
if (!res.data)
return;
const landed = new Date(value);
const futureDate = landed.getDate() + res.data.dayDuration;
landed.setDate(futureDate);
this.travel.landed = landed;
this.travel.warehouseInFk = res.data.warehouseInFk;
this.travel.warehouseOutFk = res.data.warehouseOutFk;
});
}
onSubmit() {
return this.$.watcher.submit().then(
res => this.$state.go('travel.card.basicData', {id: res.data.id})

View File

@ -5,10 +5,12 @@ describe('Travel Component vnTravelCreate', () => {
let $scope;
let $state;
let controller;
let $httpBackend;
beforeEach(ngModule('travel'));
beforeEach(inject(($componentController, $rootScope, _$state_) => {
beforeEach(inject(($componentController, $rootScope, _$state_, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$state = _$state_;
$scope.watcher = watcher;
@ -38,4 +40,49 @@ describe('Travel Component vnTravelCreate', () => {
expect(controller.travel).toEqual(json);
});
});
describe('onShippedChange()', () => {
it(`should do nothing if there's no agencyModeFk in the travel.`, () => {
controller.travel = {};
controller.onShippedChange();
expect(controller.travel.landed).toBeUndefined();
expect(controller.travel.warehouseInFk).toBeUndefined();
expect(controller.travel.warehouseOutFk).toBeUndefined();
});
it(`should do nothing if there's no response data.`, () => {
controller.travel = {agencyModeFk: 4};
const tomorrow = new Date();
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;
$httpBackend.expectGET(query).respond(undefined);
controller.onShippedChange(tomorrow);
$httpBackend.flush();
expect(controller.travel.warehouseInFk).toBeUndefined();
expect(controller.travel.warehouseOutFk).toBeUndefined();
expect(controller.travel.dayDuration).toBeUndefined();
});
it(`should fill the fields when it's selected a date and agency.`, () => {
controller.travel = {agencyModeFk: 1};
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 9);
const expectedResponse = {
id: 8,
dayDuration: 9,
warehouseInFk: 5,
warehouseOutFk: 1
};
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;
$httpBackend.expectGET(query).respond(expectedResponse);
controller.onShippedChange(tomorrow);
$httpBackend.flush();
expect(controller.travel.warehouseInFk).toEqual(expectedResponse.warehouseInFk);
expect(controller.travel.warehouseOutFk).toEqual(expectedResponse.warehouseOutFk);
});
});
});

View File

@ -42,6 +42,11 @@
"model": "Account",
"foreignKey": "userFk"
},
"boss": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "bossFk"
},
"client": {
"type": "belongsTo",
"model": "Client",

View File

@ -29,6 +29,15 @@
ng-model="$ctrl.worker.phone"
rule>
</vn-textfield>
<vn-autocomplete
disabled="false"
ng-model="$ctrl.worker.bossFk"
url="Clients/activeWorkersWithRole"
show-field="nickname"
search-function="{firstName: $search}"
where="{role: 'employee'}"
label="Boss">
</vn-autocomplete>
</vn-horizontal>
</vn-vertical>
</vn-card>

View File

@ -7,6 +7,7 @@ Extension: Extensión
Fiscal identifier: NIF
Go to client: Ir al cliente
Last name: Apellidos
Boss: Jefe
Log: Historial
Private Branch Exchange: Centralita
Role: Rol

View File

@ -30,6 +30,14 @@
<vn-label-value label="Department"
value="{{worker.department.department.name}}">
</vn-label-value>
<vn-label-value
label="Boss">
<span
ng-click="workerDescriptor.show($event, worker.boss.id)"
class="link">
{{::worker.boss.nickname}}
</span>
</vn-label-value>
<vn-label-value label="Phone"
value="{{worker.phone}}">
</vn-label-value>
@ -50,4 +58,7 @@
</vn-label-value>
</vn-one>
</vn-horizontal>
</vn-card>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -11,8 +11,8 @@ class Controller extends Summary {
this.$.worker = null;
if (!value) return;
let query = `Workers/${value.id}`;
let filter = {
const query = `Workers/${value.id}`;
const filter = {
include: [
{
relation: 'user',
@ -31,13 +31,20 @@ class Controller extends Summary {
}
}]
}
}, {
},
{
relation: 'client',
scope: {fields: ['fi']}
}, {
},
{
relation: 'boss',
scope: {fields: ['id', 'nickname']}
},
{
relation: 'sip',
scope: {fields: ['extension']}
}, {
},
{
relation: 'department',
scope: {
include: {

696
package-lock.json generated
View File

@ -10606,15 +10606,50 @@
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
"bluebird": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
"integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
},
"bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha1-4Fpj95amwf8l9Hcex62twUjAcjM="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
}
}
},
"minizlib": {
"version": "1.2.1",
@ -11677,11 +11712,14 @@
}
}
},
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
"color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
"integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
"requires": {
"color-convert": "^1.9.1",
"color-string": "^1.5.4"
}
},
"ansi-styles": {
"version": "4.2.1",
@ -11693,15 +11731,19 @@
"color-convert": "^2.0.1"
}
},
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz",
"integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"binary-extensions": {
"version": "2.0.0",
@ -14271,15 +14313,18 @@
"@types/yargs-parser": "*"
}
},
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
"file-type": {
"version": "10.11.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz",
"integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="
},
"filed-mimefix": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/filed-mimefix/-/filed-mimefix-0.1.3.tgz",
"integrity": "sha1-Cwtn0HWmP8dPJv3znH+dQxSWe7U=",
"requires": {
"mime": "^1.4.0"
}
},
"chalk": {
"version": "4.1.0",
@ -14460,30 +14505,33 @@
"write-file-atomic": "^3.0.0"
}
},
"@jest/types": {
"version": "26.0.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-26.0.1.tgz",
"integrity": "sha512-IbtjvqI9+eS1qFnOIEL7ggWmT+iK/U+Vde9cGWtYb/b6XgKb3X44ZAe/z9YZzoAAZ/E92m0DqrilF934IGNnQA==",
"dev": true,
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^1.1.1",
"@types/yargs": "^15.0.0",
"chalk": "^4.0.0"
}
},
"@types/babel__core": {
"version": "7.1.8",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.8.tgz",
"integrity": "sha512-KXBiQG2OXvaPWFPDS1rD8yV9vO0OuWIqAEqLsbfX0oU2REN5KuoMnZ1gClWcBhO5I3n6oTVAmrMufOvRqdmFTQ==",
"dev": true,
"requires": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
"fs-mkdirp-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz",
"integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.11",
"through2": "^2.0.3"
},
"dependencies": {
"through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"dev": true,
"requires": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
"dev": true
}
}
},
"@types/yargs": {
"version": "15.0.5",
@ -15323,18 +15371,73 @@
"@jest/types": "^26.0.1"
}
},
"jest-util": {
"version": "26.0.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.0.1.tgz",
"integrity": "sha512-byQ3n7ad1BO/WyFkYvlWQHTsomB6GIewBh8tlGtusiylAlaxQ1UpS0XYH0ngOyhZuHVLN79Qvl6/pMiDMSSG1g==",
"dev": true,
"requires": {
"@jest/types": "^26.0.1",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.4",
"is-ci": "^2.0.0",
"make-dir": "^3.0.0"
}
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ienoopen": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz",
"integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ=="
},
"iferr": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz",
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true
},
"ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
"dev": true
},
"image-type": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
"integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==",
"requires": {
"file-type": "^10.10.0"
}
},
"imap": {
"version": "0.8.19",
"resolved": "https://registry.npmjs.org/imap/-/imap-0.8.19.tgz",
"integrity": "sha1-NniHOTSrCc6mukh0HyhNoq9Z2NU=",
"requires": {
"readable-stream": "1.1.x",
"utf7": ">=1.0.2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"make-dir": {
"version": "3.1.0",
@ -17243,28 +17346,53 @@
"to-regex-range": "^5.0.1"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"dev": true,
"optional": true
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
"mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
"integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==",
"dev": true,
"requires": {
"concat-stream": "^1.5.0",
"duplexify": "^3.4.2",
"end-of-stream": "^1.1.0",
"flush-write-stream": "^1.0.0",
"from2": "^2.1.0",
"parallel-transform": "^1.1.0",
"pump": "^3.0.0",
"pumpify": "^1.3.3",
"stream-each": "^1.1.0",
"through2": "^2.0.0"
},
"dependencies": {
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"dev": true,
"requires": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
}
}
},
"graceful-fs": {
"version": "4.2.4",
@ -17455,20 +17583,23 @@
"picomatch": "^2.0.5"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
"nocache": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
"integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
"node-abi": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.19.3.tgz",
"integrity": "sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg==",
"requires": {
"semver": "^5.4.1"
}
},
"node-addon-api": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
},
"pretty-format": {
"version": "26.0.1",
@ -17992,11 +18123,10 @@
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"is-ci": {
"version": "2.0.0",
@ -18455,16 +18585,58 @@
}
}
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"dev": true,
"requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
"postcss-value-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
"dev": true
},
"prebuild-install": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.0.1.tgz",
"integrity": "sha512-7GOJrLuow8yeiyv75rmvZyeMGzl8mdEX5gY69d6a6bHWmiPevwqFw+tQavhK0EYMaSg3/KD24cWqeQv1EWsqDQ==",
"requires": {
"detect-libc": "^1.0.3",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^2.7.0",
"noop-logger": "^0.1.1",
"npmlog": "^4.0.1",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^3.0.3",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0",
"which-pm-runs": "^1.0.0"
},
"dependencies": {
"decompress-response": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"requires": {
"mimic-response": "^2.0.0"
}
},
"mimic-response": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
},
"simple-get": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
"integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
"requires": {
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
}
}
},
"ws": {
"version": "7.3.0",
@ -19264,15 +19436,25 @@
"path-key": "^3.0.0"
}
},
"os-locale": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-5.0.0.tgz",
"integrity": "sha512-tqZcNEDAIZKBEPnHPlVDvKrp7NzgLi7jRmhKiUoa2NUmhl13FtkAGLUVR+ZsYvApBQdBfYm43A4tXXQ4IrYLBA==",
"requires": {
"execa": "^4.0.0",
"lcid": "^3.0.0",
"mem": "^5.0.0"
}
"read-chunk": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz",
"integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==",
"requires": {
"pify": "^4.0.1",
"with-open-file": "^0.1.6"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
"integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
"dev": true,
"requires": {
"load-json-file": "^1.0.0",
"normalize-package-data": "^2.3.2",
"path-type": "^1.0.0"
}
},
"path-key": {
"version": "3.1.1",
@ -21962,15 +22144,50 @@
"ms": "2.1.2"
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
"sharp": {
"version": "0.27.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.27.1.tgz",
"integrity": "sha512-IQNXWdspb4nZcJemXa6cfgz+JvKONsuqP8Mwi1Oti23Uo7+J+UF2jihJDf6I1BQbrmhcZ0lagH/1WYG+ReAzyQ==",
"requires": {
"array-flatten": "^3.0.0",
"color": "^3.1.3",
"detect-libc": "^1.0.3",
"node-addon-api": "^3.1.0",
"npmlog": "^4.1.2",
"prebuild-install": "^6.0.0",
"semver": "^7.3.4",
"simple-get": "^4.0.0",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
},
"dependencies": {
"array-flatten": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz",
"integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
},
"https-proxy-agent": {
"version": "4.0.0",
@ -24125,74 +24342,64 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
}
}
},
"strong-globalize": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/strong-globalize/-/strong-globalize-4.1.3.tgz",
"integrity": "sha512-SJegV7w5D4AodEspZJtJ7rls3fmi+Zc0PdyJCqBsg4RN9B8TC80/uAI2fikC+s1Jp9FLvr2vDX8f0Fqc62M4OA==",
"requires": {
"accept-language": "^3.0.18",
"debug": "^4.1.1",
"globalize": "^1.4.2",
"lodash": "^4.17.4",
"md5": "^2.2.1",
"mkdirp": "^0.5.1",
"os-locale": "^3.1.0",
"yamljs": "^0.3.0"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
"tar-fs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"strong-remoting": {
"version": "3.16.2",
"resolved": "https://registry.npmjs.org/strong-remoting/-/strong-remoting-3.16.2.tgz",
"integrity": "sha512-Kj8dZh6q0F3GQTil2rkEt6WuQoQbXFRZ2AJG36iBPmxcVEE9bVhWXUYxcJIxXaGekCprxp8kfdLLN67yp0J4Hg==",
"requires": {
"async": "^3.1.0",
"body-parser": "^1.12.4",
"debug": "^4.1.1",
"depd": "^2.0.0",
"escape-string-regexp": "^2.0.0",
"eventemitter2": "^5.0.1",
"express": "4.x",
"inflection": "^1.7.1",
"jayson": "^2.0.5",
"js2xmlparser": "^3.0.0",
"loopback-datatype-geopoint": "^1.0.0",
"loopback-phase": "^3.1.0",
"mux-demux": "^3.7.9",
"qs": "^6.2.1",
"request": "^2.83.0",
"sse": "0.0.8",
"strong-error-handler": "^3.0.0",
"strong-globalize": "^5.0.2",
"traverse": "^0.6.6",
"xml2js": "^0.4.8"
},
"dependencies": {
"async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"dependencies": {
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"cross-spawn": {
"version": "7.0.3",
@ -24609,14 +24816,38 @@
"readable-stream": "^3.4.0"
}
},
"buffer": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
"integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
"uid2": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
},
"unbzip2-stream": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
"integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"through": "^2.3.8"
},
"dependencies": {
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true
},
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
}
}
},
"readable-stream": {
"version": "3.6.0",
@ -26380,14 +26611,41 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"dev": true
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
"with-open-file": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
"integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==",
"requires": {
"p-finally": "^1.0.0",
"p-try": "^2.1.0",
"pify": "^4.0.1"
}
},
"word-count": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/word-count/-/word-count-0.2.2.tgz",
"integrity": "sha1-aZGS/KaCn+k21Byw2V25JIxXBFE="
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
"dev": true
},
"worker-farm": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz",
"integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==",
"dev": true,
"requires": {
"errno": "~0.1.7"
}
},
"semver": {
"version": "6.3.0",

View File

@ -12,10 +12,12 @@
"node": ">=12"
},
"dependencies": {
"bmp-js": "^0.1.0",
"compression": "^1.7.3",
"fs-extra": "^5.0.0",
"helmet": "^3.21.2",
"i18n": "^0.8.4",
"image-type": "^4.1.0",
"imap": "^0.8.19",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
@ -30,10 +32,11 @@
"node-ssh": "^11.0.0",
"object-diff": "0.0.4",
"object.pick": "^1.3.0",
"read-chunk": "^3.2.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.8",
"require-yaml": "0.0.1",
"sharp": "^0.25.4",
"sharp": "^0.27.1",
"smbhash": "0.0.1",
"soap": "^0.35.0",
"strong-error-handler": "^2.3.2",