Merge branch 'dev' into 4074-download-user-ACL
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alexandre Riera 2023-05-04 12:39:55 +00:00
commit ede35db638
308 changed files with 7900 additions and 2443 deletions

View File

@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2316.01] - 2023-05-04
### Added
-
- (Usuarios -> Histórico) Nueva sección
- (Roles -> Histórico) Nueva sección
### Changed
- (Artículo -> Precio fijado) Modificado el buscador superior por uno lateral

1
Jenkinsfile vendored
View File

@ -52,6 +52,7 @@ pipeline {
}}}
environment {
NODE_ENV = ""
TZ = 'Europe/Madrid'
}
parallel {
stage('Frontend') {

View File

@ -0,0 +1,17 @@
name: account
columns:
id: id
name: name
roleFk: role
nickname: nickname
lang: lang
password: password
bcryptPassword: bcrypt password
active: active
email: email
emailVerified: email verified
created: created
updated: updated
image: image
hasGrant: has grant
userFk: user

View File

@ -0,0 +1,17 @@
name: cuenta
columns:
id: id
name: nombre
roleFk: rol
nickname: apodo
lang: idioma
password: contraseña
bcryptPassword: contraseña bcrypt
active: activo
email: email
emailVerified: email verificado
created: creado
updated: actualizado
image: imagen
hasGrant: tiene permiso
userFk: usuario

View File

@ -40,8 +40,7 @@ module.exports = Self => {
try {
const sale = await models.Sale.findById(saleId, null, myOptions);
const saleUpdated = await sale.updateAttributes({
originalQuantity: sale.quantity,
quantity: quantity
quantity
}, myOptions);
if (tx) await tx.commit();

View File

@ -30,7 +30,7 @@ describe('setSaleQuantity()', () => {
await models.Collection.setSaleQuantity(saleId, newQuantity, options);
const updateSale = await models.Sale.findById(saleId, null, options);
expect(updateSale.originalQuantity).toEqual(originalSale.quantity);
expect(updateSale.quantity).not.toEqual(originalSale.quantity);
expect(updateSale.quantity).toEqual(newQuantity);
await tx.rollback();

130
back/methods/image/scrub.js Normal file
View File

@ -0,0 +1,130 @@
const fs = require('fs-extra');
const path = require('path');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('scrub', {
description: 'Deletes images without database reference',
accessType: 'WRITE',
accepts: [
{
arg: 'collection',
type: 'string',
description: 'The collection name',
required: true
}, {
arg: 'remove',
type: 'boolean',
description: 'Delete instead of move images to trash'
}, {
arg: 'limit',
type: 'integer',
description: 'Maximum number of images to clean'
}, {
arg: 'dryRun',
type: 'boolean',
description: 'Simulate actions'
}, {
arg: 'skipLock',
type: 'boolean',
description: 'Wether to skip exclusive lock'
}
],
returns: {
type: 'integer',
root: true
},
http: {
path: `/scrub`,
verb: 'POST'
}
});
Self.scrub = async function(collection, remove, limit, dryRun, skipLock) {
const $ = Self.app.models;
const env = process.env.NODE_ENV;
dryRun = dryRun || (env && env !== 'production');
const instance = await $.ImageCollection.findOne({
fields: ['id'],
where: {name: collection}
});
if (!instance)
throw new UserError('Collection does not exist');
const container = await $.ImageContainer.container(collection);
const rootPath = container.client.root;
let tx;
let opts;
const lockName = 'salix.Image.scrub';
if (!skipLock) {
tx = await Self.beginTransaction({timeout: null});
opts = {transaction: tx};
const [row] = await Self.rawSql(
`SELECT GET_LOCK(?, 10) hasLock`, [lockName], opts);
if (!row.hasLock)
throw new UserError('Cannot obtain exclusive lock');
}
try {
const now = Date.vnNew().toJSON();
const scrubDir = path.join(rootPath, '.scrub', now);
const collectionDir = path.join(rootPath, collection);
const sizes = await fs.readdir(collectionDir);
let cleanCount = 0;
mainLoop: for (const size of sizes) {
const sizeDir = path.join(collectionDir, size);
const scrubSizeDir = path.join(scrubDir, collection, size);
const images = await fs.readdir(sizeDir);
for (const image of images) {
const imageName = path.parse(image).name;
const count = await Self.count({
collectionFk: collection,
name: imageName
}, opts);
const exists = count > 0;
let scrubDirCreated = false;
if (!exists) {
const srcFile = path.join(sizeDir, image);
if (remove !== true) {
if (!scrubDirCreated) {
if (!dryRun)
await fs.mkdir(scrubSizeDir, {recursive: true});
scrubDirCreated = true;
}
const dstFile = path.join(scrubSizeDir, image);
if (!dryRun) await fs.rename(srcFile, dstFile);
} else {
try {
if (!dryRun) await fs.unlink(srcFile);
} catch (err) {
console.error(err.message);
}
}
cleanCount++;
if (limit && cleanCount == limit)
break mainLoop;
}
}
}
return cleanCount;
} finally {
if (!skipLock) {
try {
await Self.rawSql(`DO RELEASE_LOCK(?)`, [lockName], opts);
await tx.rollback();
} catch (err) {
console.error(err.message);
}
}
}
};
};

View File

@ -12,13 +12,13 @@ module.exports = Self => {
type: 'Number',
description: 'The entity id',
required: true
},
{
}, {
arg: 'collection',
type: 'string',
description: 'The collection name',
required: true
}],
}
],
returns: {
type: 'Object',
root: true

View File

@ -5,6 +5,7 @@ const gm = require('gm');
module.exports = Self => {
require('../methods/image/download')(Self);
require('../methods/image/upload')(Self);
require('../methods/image/scrub')(Self);
Self.resize = async function({collectionName, srcFile, fileName, entityId}) {
const models = Self.app.models;
@ -29,13 +30,14 @@ module.exports = Self => {
);
// Insert image row
const imageName = path.parse(fileName).name;
await models.Image.upsertWithWhere(
{
name: fileName,
name: imageName,
collectionFk: collectionName
},
{
name: fileName,
name: imageName,
collectionFk: collectionName,
updated: Date.vnNow() / 1000,
}
@ -49,7 +51,7 @@ module.exports = Self => {
if (entity) {
await entity.updateAttribute(
collection.property,
fileName
imageName
);
}

View File

@ -53,7 +53,7 @@ async function test() {
const JunitReporter = require('jasmine-reporters');
jasmine.addReporter(new JunitReporter.JUnitXmlReporter());
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000;
jasmine.exitOnCompletion = true;
}

View File

@ -8,7 +8,7 @@ CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`invoiceOut_new`(
BEGIN
/**
* Creación de facturas emitidas.
* requiere previamente tabla ticketToInvoice(id).
* requiere previamente tabla tmp.ticketToInvoice(id).
*
* @param vSerial serie a la cual se hace la factura
* @param vInvoiceDate fecha de la factura
@ -36,13 +36,13 @@ BEGIN
SELECT t.clientFk, t.companyFk
INTO vClient, vCompany
FROM ticketToInvoice tt
FROM tmp.ticketToInvoice tt
JOIN ticket t ON t.id = tt.id
LIMIT 1;
-- Eliminem de ticketToInvoice els tickets que no han de ser facturats
-- Eliminem de tmp.ticketToInvoice els tickets que no han de ser facturats
DELETE ti.*
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
JOIN ticket t ON t.id = ti.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
@ -57,7 +57,7 @@ BEGIN
SELECT SUM(s.quantity * s.price * (100 - s.discount)/100), ts.id
INTO vIsAnySaleToInvoice, vIsAnyServiceToInvoice
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
LEFT JOIN sale s ON s.ticketFk = t.id
LEFT JOIN ticketService ts ON ts.ticketFk = t.id;
@ -100,13 +100,13 @@ BEGIN
WHERE id = vNewInvoiceId;
UPDATE ticket t
JOIN ticketToInvoice ti ON ti.id = t.id
JOIN tmp.ticketToInvoice ti ON ti.id = t.id
SET t.refFk = vNewRef;
DROP TEMPORARY TABLE IF EXISTS tmp.updateInter;
CREATE TEMPORARY TABLE tmp.updateInter ENGINE = MEMORY
SELECT s.id,ti.id ticket_id,vWorker Id_Trabajador
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
LEFT JOIN ticketState ts ON ti.id = ts.ticket
JOIN state s
WHERE IFNULL(ts.alertLevel,0) < 3 and s.`code` = getAlert3State(ti.id);
@ -116,7 +116,7 @@ BEGIN
INSERT INTO ticketLog (action, userFk, originFk, description)
SELECT 'UPDATE', account.myUser_getId(), ti.id, CONCAT('Crea factura ', vNewRef)
FROM ticketToInvoice ti;
FROM tmp.ticketToInvoice ti;
CALL invoiceExpenceMake(vNewInvoiceId);
CALL invoiceTaxMake(vNewInvoiceId,vTaxArea);
@ -159,7 +159,7 @@ BEGIN
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
FROM tmp.ticketToInvoice;
CALL `ticket_getTax`('NATIONAL');
@ -220,6 +220,6 @@ BEGIN
END IF;
DROP TEMPORARY TABLE `ticketToInvoice`;
DROP TEMPORARY TABLE `tmp`.`ticketToInvoice`;
END$$
DELIMITER ;

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Receipt', 'receiptEmail', '*', 'ALLOW', 'ROLE', 'salesAssistant');

View File

@ -10,7 +10,7 @@ CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`invoiceOut_new`(
BEGIN
/**
* Creación de facturas emitidas.
* requiere previamente tabla ticketToInvoice(id).
* requiere previamente tabla tmp.ticketToInvoice(id).
*
* @param vSerial serie a la cual se hace la factura
* @param vInvoiceDate fecha de la factura
@ -49,7 +49,7 @@ BEGIN
vCompanyFk,
vMaxShipped,
vIsCorrectInvoiceDate
FROM ticketToInvoice tt
FROM tmp.ticketToInvoice tt
JOIN ticket t ON t.id = tt.id;
IF(vMaxShipped > vInvoiceDate) THEN
@ -60,9 +60,9 @@ BEGIN
CALL util.throw('Exists an invoice with a previous date');
END IF;
-- Eliminem de ticketToInvoice els tickets que no han de ser facturats
-- Eliminem de tmp.ticketToInvoice els tickets que no han de ser facturats
DELETE ti.*
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
JOIN ticket t ON t.id = ti.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
@ -77,11 +77,11 @@ BEGIN
SELECT SUM(s.quantity * s.price * (100 - s.discount)/100) <> 0
INTO vIsAnySaleToInvoice
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
JOIN sale s ON s.ticketFk = t.id;
SELECT COUNT(*) > 0 INTO vIsAnyServiceToInvoice
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
JOIN ticketService ts ON ts.ticketFk = t.id;
IF (vIsAnySaleToInvoice OR vIsAnyServiceToInvoice)
@ -121,13 +121,13 @@ BEGIN
WHERE id = vNewInvoiceId;
UPDATE ticket t
JOIN ticketToInvoice ti ON ti.id = t.id
JOIN tmp.ticketToInvoice ti ON ti.id = t.id
SET t.refFk = vNewRef;
DROP TEMPORARY TABLE IF EXISTS tmp.updateInter;
CREATE TEMPORARY TABLE tmp.updateInter ENGINE = MEMORY
SELECT s.id,ti.id ticket_id,vWorker Id_Trabajador
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
LEFT JOIN ticketState ts ON ti.id = ts.ticket
JOIN state s
WHERE IFNULL(ts.alertLevel,0) < 3 and s.`code` = getAlert3State(ti.id);
@ -137,7 +137,7 @@ BEGIN
INSERT INTO ticketLog (action, userFk, originFk, description)
SELECT 'UPDATE', account.myUser_getId(), ti.id, CONCAT('Crea factura ', vNewRef)
FROM ticketToInvoice ti;
FROM tmp.ticketToInvoice ti;
CALL invoiceExpenceMake(vNewInvoiceId);
CALL invoiceTaxMake(vNewInvoiceId,vTaxArea);
@ -175,7 +175,7 @@ BEGIN
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
FROM tmp.ticketToInvoice;
CALL `ticket_getTax`('NATIONAL');
@ -253,6 +253,6 @@ BEGIN
DROP TEMPORARY TABLE tmp.ticketServiceTax;
END IF;
END IF;
DROP TEMPORARY TABLE `ticketToInvoice`;
DROP TEMPORARY TABLE tmp.`ticketToInvoice`;
END$$
DELIMITER ;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`printQueueArgs` MODIFY COLUMN value varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NULL;

View File

@ -0,0 +1,5 @@
DROP TRIGGER `vn`.`deviceProduction_afterInsert`;
DROP TRIGGER `vn`.`deviceProduction_afterUpdate`;
DROP TRIGGER `vn`.`deviceProductionUser_afterDelete`;

View File

@ -1,12 +1,5 @@
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;
DROP PROCEDURE IF EXISTS `vn`.`route_getTickets`;
DELIMITER $$
$$
@ -17,14 +10,14 @@ BEGIN
* de sus tickets.
*
* @param vRouteFk
*
* @select Información de los tickets
*/
SELECT
t.id Id,
SELECT *
FROM (
SELECT t.id Id,
t.clientFk Client,
a.id Address,
a.nickname ClientName,
t.packages Packages,
a.street AddressName,
a.postalCode PostalCode,
@ -37,34 +30,48 @@ BEGIN
d.longitude Longitude,
d.latitude Latitude,
wm.mediaValue SalePersonPhone,
tob.Note Note,
t.isSigned Signed
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 t.id = d.ticketFk
LEFT JOIN delivery d ON d.ticketFk = t.id
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
LEFT JOIN
(SELECT tob.description Note, t.id
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
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 t.id, i.itemPackingTypeFk
)sub
GROUP BY sub.ticketFk
) sub2 ON sub2.ticketFk = t.id
)sub2 ON sub2.ticketFk = t.id
WHERE t.routeFk = vRouteFk
GROUP BY t.id
ORDER BY t.priority;
ORDER BY d.id DESC
LIMIT 10000000000000000000
)sub3
GROUP BY sub3.id
ORDER BY sub3.priority;
END$$
DELIMITER ;
ALTER TABLE `vn`.`delivery` DROP FOREIGN KEY 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`);

View File

@ -0,0 +1,3 @@
UPDATE `salix`.`ACL`
SET model = 'InvoiceOut'
WHERE property IN ('negativeBases', 'negativeBasesCsv');

View File

@ -1,21 +0,0 @@
create or replace definer = root@localhost view User as
select `account`.`user`.`id` AS `id`,
`account`.`user`.`realm` AS `realm`,
`account`.`user`.`name` AS `name`,
`account`.`user`.`nickname` AS `nickname`,
`account`.`user`.`bcryptPassword` AS `password`,
`account`.`user`.`role` AS `role`,
`account`.`user`.`active` AS `active`,
`account`.`user`.`email` AS `email`,
`account`.`user`.`emailVerified` AS `emailVerified`,
`account`.`user`.`verificationToken` AS `verificationToken`,
`account`.`user`.`lang` AS `lang`,
`account`.`user`.`lastPassChange` AS `lastPassChange`,
`account`.`user`.`created` AS `created`,
`account`.`user`.`updated` AS `updated`,
`account`.`user`.`image` AS `image`,
`account`.`user`.`recoverPass` AS `recoverPass`,
`account`.`user`.`sync` AS `sync`,
`account`.`user`.`hasGrant` AS `hasGrant`
from `account`.`user`;

View File

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

View File

@ -0,0 +1,16 @@
ALTER TABLE `vn`.`client` ADD rating INT UNSIGNED DEFAULT NULL NULL COMMENT 'información proporcionada por Informa';
ALTER TABLE `vn`.`client` ADD recommendedCredit INT UNSIGNED DEFAULT NULL NULL COMMENT 'información proporcionada por Informa';
CREATE TABLE `vn`.`clientInforma` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`clientFk` int(11) NOT NULL,
`rating` int(10) unsigned DEFAULT NULL,
`recommendedCredit` int(10) unsigned DEFAULT NULL,
`workerFk` int(10) unsigned NOT NULL,
`created` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `informaWorkers_fk_idx` (`workerFk`),
KEY `informaClientFk` (`clientFk`),
CONSTRAINT `informa_ClienteFk` FOREIGN KEY (`clientFk`) REFERENCES `client` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `informa_workers_fk` FOREIGN KEY (`workerFk`) REFERENCES `worker` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci COMMENT='información proporcionada por Informa, se actualiza desde el hook de client (salix)';

View File

@ -0,0 +1,64 @@
DELETE FROM `salix`.`ACL` WHERE id=7;
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Client', 'setRating', 'WRITE', 'ALLOW', 'ROLE', 'financial');
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('Client', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Client', 'addressesPropagateRe', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'canBeInvoiced', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'canCreateTicket', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'consumption', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'createAddress', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'createWithUser', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'extendedListFilter', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'getAverageInvoiced', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'getCard', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'getDebt', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'getMana', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'transactions', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'hasCustomerRole', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'isValidClient', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'lastActiveTickets', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'sendSms', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'setPassword', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'summary', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateAddress', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateFiscalData', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateUser', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'uploadFile', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientWelcomeHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientWelcomeEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'printerSetupHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'printerSetupEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'sepaCoreEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorStHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorStEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorNdHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'letterDebtorNdEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'clientDebtStatementEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'creditRequestEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationHtml', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'incotermsAuthorizationEmail', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'consumptionSendQueued', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'filter', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'getClientOrSupplierReference', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'upsert', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'create', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'replaceById', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateAttributes', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateAttributes', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'deleteById', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'replaceOrCreate', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateAll', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'upsertWithWhere', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,2 @@
DROP PROCEDURE `vn`.`refund`;
DROP PROCEDURE `vn`.`ticket_doRefund`;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`entry` DROP COLUMN `notes`;

View File

@ -0,0 +1,9 @@
-- vn.companyI18n definition
CREATE TABLE `vn`.`companyI18n` (
`companyFk` smallint(5) unsigned NOT NULL,
`lang` char(2) CHARACTER SET utf8mb3 NOT NULL,
`footnotes` longtext COLLATE utf8mb3_unicode_ci DEFAULT NULL,
PRIMARY KEY (`companyFk`,`lang`),
CONSTRAINT `companyI18n_FK` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`company` ADD `web` varchar(100) NULL;

View File

@ -0,0 +1,5 @@
DROP PROCEDURE IF EXISTS `vn`.`sale_setQuantity`;
DROP PROCEDURE IF EXISTS `vn`.`collection_updateSale`;
DROP PROCEDURE IF EXISTS `vn`.`replaceMovimientosMark`;
DROP PROCEDURE IF EXISTS `vn`.`saleTracking_Replace`;
DROP PROCEDURE IF EXISTS `vn`.`sale_updateOriginalQuantity`;

View File

@ -0,0 +1,5 @@
UPDATE vn.supplier s
JOIN vn.country c ON c.id = s.countryFk
SET s.nif = MID(REPLACE(s.nif, ' ', ''), 3, LENGTH(REPLACE(s.nif, ' ', '')) - 1)
WHERE s.isVies = TRUE
AND c.code = LEFT(REPLACE(s.nif, ' ', ''), 2);

View File

@ -0,0 +1,5 @@
UPDATE IGNORE vn.client c
JOIN vn.country co ON co.id = c.countryFk
SET c.fi = MID(REPLACE(c.fi, ' ', ''), 3, LENGTH(REPLACE(c.fi, ' ', '')) - 1)
WHERE c.isVies = TRUE
AND co.code = LEFT(REPLACE(c.fi, ' ', ''), 2);

View File

@ -173,10 +173,6 @@ INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPrepare
(1, 'First sector', 1, 1, 'FIRST'),
(2, 'Second sector', 2, 0, 'SECOND');
INSERT INTO `vn`.`operator` (`workerFk`, `numberOfWagons`, `trainFk`, `itemPackingTypeFk`, `warehouseFk`, `sectorFk`, `labelerFk`)
VALUES ('1106', '1', '1', 'H', '1', '1', '1'),
('1107', '1', '1', 'V', '1', '2', '1');
INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAddress`)
VALUES
(1, 'printer1', 'path1', 0, 1 , NULL),
@ -550,7 +546,8 @@ INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`
VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
(2, 'Farmer King', 'The farmer', 4000020002, 1, '87945234L', 0, util.VN_CURDATE(), 1, 'supplier address 2', 'GOTHAM', 2, 43022, 1, 2, 10, 93, 2, 8, 18, 'animals', 1, '400664487V'),
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
(442, 'Verdnatura Levante SL', 'Verdnatura', 5115000442, 1, '06815934E', 0, util.VN_CURDATE(), 1, 'supplier address 3', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185000440, 1, '03815934E', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES
@ -1200,6 +1197,11 @@ INSERT INTO `vn`.`train`(`id`, `name`)
(1, 'Train1'),
(2, 'Train2');
INSERT INTO `vn`.`operator` (`workerFk`, `numberOfWagons`, `trainFk`, `itemPackingTypeFk`, `warehouseFk`, `sectorFk`, `labelerFk`)
VALUES
('1106', '1', '1', 'H', '1', '1', '1'),
('1107', '1', '1', 'V', '1', '1', '1');
INSERT INTO `vn`.`collection`(`id`, `workerFk`, `stateFk`, `created`, `trainFk`)
VALUES
(1, 1106, 5, DATE_ADD(util.VN_CURDATE(),INTERVAL +1 DAY), 1),
@ -1407,16 +1409,16 @@ INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseO
(7, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 4, 1, 50.00, 500, 'seventh travel', 2, 1),
(8, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 1, 1, 50.00, 500, 'eight travel', 1, 2);
INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `isConfirmed`, `companyFk`, `invoiceNumber`, `reference`, `isExcludedFromAvailable`, `isRaid`, `notes`, `evaNotes`)
INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `isConfirmed`, `companyFk`, `invoiceNumber`, `reference`, `isExcludedFromAvailable`, `isRaid`, `evaNotes`)
VALUES
(1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 1, 442, 'IN2001', 'Movement 1', 0, 0, '', ''),
(2, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 442, 'IN2002', 'Movement 2', 0, 0, 'this is the note two', 'observation two'),
(3, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 3, 0, 442, 'IN2003', 'Movement 3', 0, 0, 'this is the note three', 'observation three'),
(4, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 69, 'IN2004', 'Movement 4', 0, 0, 'this is the note four', 'observation four'),
(5, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 0, 442, 'IN2005', 'Movement 5', 0, 0, 'this is the note five', 'observation five'),
(6, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 6, 0, 442, 'IN2006', 'Movement 6', 0, 0, 'this is the note six', 'observation six'),
(7, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2007', 'Movement 7', 0, 0, 'this is the note seven', 'observation seven'),
(8, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2008', 'Movement 8', 1, 1, '', '');
(1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 1, 442, 'IN2001', 'Movement 1', 0, 0, ''),
(2, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 442, 'IN2002', 'Movement 2', 0, 0, 'observation two'),
(3, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 3, 0, 442, 'IN2003', 'Movement 3', 0, 0, 'observation three'),
(4, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 69, 'IN2004', 'Movement 4', 0, 0, 'observation four'),
(5, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 0, 442, 'IN2005', 'Movement 5', 0, 0, 'observation five'),
(6, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 6, 0, 442, 'IN2006', 'Movement 6', 0, 0, 'observation six'),
(7, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2007', 'Movement 7', 0, 0, 'observation seven'),
(8, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2008', 'Movement 8', 1, 1, '');
INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `itemFk`, `itemTypeFk`, `saleTotal`, `saleWaste`, `rate`)
VALUES
@ -2788,7 +2790,7 @@ INSERT INTO `vn`.`profileType` (`id`, `name`)
INSERT INTO `salix`.`url` (`appName`, `environment`, `url`)
VALUES
('lilium', 'dev', 'http://localhost:8080/#/'),
('lilium', 'dev', 'http://localhost:9000/#/'),
('salix', 'dev', 'http://localhost:5000/#!/');
INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)

View File

@ -42776,7 +42776,7 @@ CREATE DEFINER=`root`@`localhost` FUNCTION `hasAnyNegativeBase`() RETURNS tinyin
BEGIN
/* Calcula si existe alguna base imponible negativa
* Requiere la tabla temporal vn.ticketToInvoice(id)
* Requiere la tabla temporal tmp.ticketToInvoice(id)
*
* returns BOOLEAN
*/
@ -42787,7 +42787,7 @@ BEGIN
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
FROM tmp.ticketToInvoice;
CALL ticket_getTax(NULL);
@ -55223,7 +55223,7 @@ DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `invoiceExpenceMake`(IN vInvoice INT)
BEGIN
/* Inserta las partidas de gasto correspondientes a la factura
* REQUIERE tabla ticketToInvoice
* REQUIERE tabla tmp.ticketToInvoice
* @param vInvoice Numero de factura
*/
DELETE FROM invoiceOutExpence
@ -55233,7 +55233,7 @@ BEGIN
SELECT vInvoice,
expenceFk,
SUM(ROUND(quantity * price * (100 - discount)/100,2)) amount
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
GROUP BY i.expenceFk
@ -55243,7 +55243,7 @@ BEGIN
SELECT vInvoice,
tst.expenceFk,
SUM(ROUND(ts.quantity * ts.price ,2)) amount
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
JOIN ticketService ts ON ts.ticketFk = t.id
JOIN ticketServiceType tst ON tst.id = ts.ticketServiceTypeFk
HAVING amount != 0;
@ -55270,9 +55270,9 @@ BEGIN
SET vMaxTicketDate = vn2008.DAYEND(vMaxTicketDate);
DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`;
DROP TEMPORARY TABLE IF EXISTS `tmp`.`ticketToInvoice`;
CREATE TEMPORARY TABLE `ticketToInvoice`
CREATE TEMPORARY TABLE `tmp`.`ticketToInvoice`
(PRIMARY KEY (`id`))
ENGINE = MEMORY
SELECT Id_Ticket id FROM vn2008.Tickets WHERE (Fecha BETWEEN vMinDateTicket
@ -55305,8 +55305,8 @@ BEGIN
SET vMinTicketDate = util.firstDayOfYear(vMaxTicketDate - INTERVAL 1 YEAR);
SET vMaxTicketDate = util.dayend(vMaxTicketDate);
DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`;
CREATE TEMPORARY TABLE `ticketToInvoice`
DROP TEMPORARY TABLE IF EXISTS `tmp`.`ticketToInvoice`;
CREATE TEMPORARY TABLE `tmp`.`ticketToInvoice`
(PRIMARY KEY (`id`))
ENGINE = MEMORY
SELECT id FROM ticket t
@ -55333,9 +55333,9 @@ DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `invoiceFromTicket`(IN vTicket INT)
BEGIN
DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`;
DROP TEMPORARY TABLE IF EXISTS `tmp`.`ticketToInvoice`;
CREATE TEMPORARY TABLE `ticketToInvoice`
CREATE TEMPORARY TABLE `tmp`.`ticketToInvoice`
(PRIMARY KEY (`id`))
ENGINE = MEMORY
SELECT id FROM vn.ticket
@ -55931,9 +55931,9 @@ BEGIN
JOIN invoiceOut io ON io.companyFk = s.id
WHERE io.id = vInvoiceFk;
DROP TEMPORARY TABLE IF EXISTS ticketToInvoice;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE ticketToInvoice
CREATE TEMPORARY TABLE tmp.ticketToInvoice
SELECT id
FROM ticket
WHERE refFk = vInvoiceRef;
@ -56408,9 +56408,9 @@ BEGIN
JOIN client c ON c.id = io.clientFk
WHERE io.id = vInvoice;
DROP TEMPORARY TABLE IF EXISTS ticketToInvoice;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE ticketToInvoice
CREATE TEMPORARY TABLE tmp.ticketToInvoice
SELECT id
FROM ticket
WHERE refFk = vInvoiceRef;
@ -56456,7 +56456,7 @@ CREATE DEFINER=`root`@`localhost` PROCEDURE `invoiceOut_exportationFromClient`(
vCompanyFk INT)
BEGIN
/**
* Genera tabla temporal ticketToInvoice necesaría para el proceso de facturación
* Genera tabla temporal tmp.ticketToInvoice necesaría para el proceso de facturación
* Los abonos quedan excluidos en las exportaciones
*
* @param vMaxTicketDate Fecha hasta la cual cogerá tickets para facturar
@ -56467,8 +56467,8 @@ BEGIN
SET vMinTicketDate = util.firstDayOfYear(vMaxTicketDate - INTERVAL 1 YEAR);
SET vMaxTicketDate = util.dayend(vMaxTicketDate);
DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`;
CREATE TEMPORARY TABLE `ticketToInvoice`
DROP TEMPORARY TABLE IF EXISTS `tmp`.`ticketToInvoice`;
CREATE TEMPORARY TABLE `tmp`.`ticketToInvoice`
(PRIMARY KEY (`id`))
ENGINE = MEMORY
SELECT t.id
@ -56503,7 +56503,7 @@ CREATE DEFINER=`root`@`localhost` PROCEDURE `invoiceOut_new`(
BEGIN
/**
* Creación de facturas emitidas.
* requiere previamente tabla ticketToInvoice(id).
* requiere previamente tabla tmp.ticketToInvoice(id).
*
* @param vSerial serie a la cual se hace la factura
* @param vInvoiceDate fecha de la factura
@ -56531,13 +56531,13 @@ BEGIN
SELECT t.clientFk, t.companyFk
INTO vClientFk, vCompanyFk
FROM ticketToInvoice tt
FROM tmp.ticketToInvoice tt
JOIN ticket t ON t.id = tt.id
LIMIT 1;
-- Eliminem de ticketToInvoice els tickets que no han de ser facturats
-- Eliminem de tmp.ticketToInvoice els tickets que no han de ser facturats
DELETE ti.*
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
JOIN ticket t ON t.id = ti.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
@ -56552,7 +56552,7 @@ BEGIN
SELECT SUM(s.quantity * s.price * (100 - s.discount)/100), ts.id
INTO vIsAnySaleToInvoice, vIsAnyServiceToInvoice
FROM ticketToInvoice t
FROM tmp.ticketToInvoice t
LEFT JOIN sale s ON s.ticketFk = t.id
LEFT JOIN ticketService ts ON ts.ticketFk = t.id;
@ -56593,13 +56593,13 @@ BEGIN
WHERE id = vNewInvoiceId;
UPDATE ticket t
JOIN ticketToInvoice ti ON ti.id = t.id
JOIN tmp.ticketToInvoice ti ON ti.id = t.id
SET t.refFk = vNewRef;
DROP TEMPORARY TABLE IF EXISTS tmp.updateInter;
CREATE TEMPORARY TABLE tmp.updateInter ENGINE = MEMORY
SELECT s.id,ti.id ticket_id,vWorker Id_Trabajador
FROM ticketToInvoice ti
FROM tmp.ticketToInvoice ti
LEFT JOIN ticketState ts ON ti.id = ts.ticket
JOIN state s
WHERE IFNULL(ts.alertLevel,0) < 3 and s.`code` = getAlert3State(ti.id);
@ -56609,7 +56609,7 @@ BEGIN
INSERT INTO ticketLog (action, userFk, originFk, description)
SELECT 'UPDATE', account.myUser_getId(), ti.id, CONCAT('Crea factura ', vNewRef)
FROM ticketToInvoice ti;
FROM tmp.ticketToInvoice ti;
CALL invoiceExpenceMake(vNewInvoiceId);
CALL invoiceTaxMake(vNewInvoiceId,vTaxArea);
@ -56647,7 +56647,7 @@ BEGIN
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
FROM tmp.ticketToInvoice;
CALL `ticket_getTax`('NATIONAL');
@ -56725,7 +56725,7 @@ BEGIN
DROP TEMPORARY TABLE tmp.ticketServiceTax;
END IF;
END IF;
DROP TEMPORARY TABLE `ticketToInvoice`;
DROP TEMPORARY TABLE `tmp`.`ticketToInvoice`;
END ;;
DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ;
@ -56876,7 +56876,7 @@ BEGIN
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
FROM tmp.ticketToInvoice;
CALL ticket_getTax(vTaxArea);
@ -68689,7 +68689,7 @@ DELIMITER ;
/*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ;
/*!50003 SET collation_connection = @saved_col_connection */ ;
/*!50003 DROP PROCEDURE IF EXISTS `ticketToInvoiceByAddress` */;
/*!50003 DROP PROCEDURE IF EXISTS `tmp`.`ticketToInvoiceByAddress` */;
/*!50003 SET @saved_cs_client = @@character_set_client */ ;
/*!50003 SET @saved_cs_results = @@character_set_results */ ;
/*!50003 SET @saved_col_connection = @@collation_connection */ ;
@ -68709,9 +68709,9 @@ BEGIN
SET vEnded = util.dayEnd(vEnded);
DROP TEMPORARY TABLE IF EXISTS vn.ticketToInvoice;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE vn.ticketToInvoice
CREATE TEMPORARY TABLE tmp.ticketToInvoice
SELECT id
FROM vn.ticket
WHERE addressFk = vAddress
@ -68745,9 +68745,9 @@ BEGIN
SET vEnded = util.dayEnd(vEnded);
DROP TEMPORARY TABLE IF EXISTS vn.ticketToInvoice;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE vn.ticketToInvoice
CREATE TEMPORARY TABLE tmp.ticketToInvoice
SELECT id
FROM vn.ticket
WHERE clientFk = vClient
@ -68808,9 +68808,9 @@ BEGIN
JOIN vn.client c ON c.id = io.clientFk
WHERE io.id = vInvoice;
DROP TEMPORARY TABLE IF EXISTS vn.ticketToInvoice;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE vn.ticketToInvoice
CREATE TEMPORARY TABLE tmp.ticketToInvoice
SELECT id
FROM vn.ticket
WHERE refFk = vInvoiceRef;

View File

@ -68,6 +68,7 @@ TABLES=(
time
volumeConfig
workCenter
companyI18n
)
dump_tables ${TABLES[@]}

View File

@ -156,14 +156,14 @@ let actions = {
await this.waitForSpinnerLoad();
},
accessToSection: async function(state) {
accessToSection: async function(state, name = 'Others') {
await this.waitForSelector('vn-left-menu');
let nested = await this.evaluate(state => {
return document.querySelector(`vn-left-menu li li > a[ui-sref="${state}"]`) != null;
}, state);
if (nested) {
let selector = 'vn-left-menu vn-item-section > vn-icon[icon=keyboard_arrow_down]';
let selector = `vn-left-menu li[name="${name}"]`;
await this.evaluate(selector => {
document.querySelector(selector).scrollIntoViewIfNeeded();
}, selector);
@ -218,6 +218,23 @@ let actions = {
return handle.jsonValue();
},
getValue: async function(selector) {
return await this.waitToGetProperty(selector, 'value');
},
getValues: async function(selectorMap) {
const values = {};
for (const key in selectorMap)
values[key] = await this.waitToGetProperty(selectorMap[key], 'value');
return values;
},
innerText: async function(selector) {
const element = await this.$(selector);
const handle = await element.getProperty('innerText');
return handle.jsonValue();
},
waitPropertyLength: async function(selector, property, minLength) {
await this.waitForFunction((selector, property, minLength) => {
const element = document.querySelector(selector);

View File

@ -283,12 +283,6 @@ export default {
cancelEditAddressButton: 'vn-client-address-edit > form > vn-button-bar > vn-button > button',
watcher: 'vn-client-address-edit vn-watcher'
},
clientWebAccess: {
enableWebAccessCheckbox: 'vn-check[label="Enable web access"]',
userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]',
email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]',
saveButton: 'button[type=submit]'
},
clientNotes: {
addNoteFloatButton: 'vn-float-button',
note: 'vn-textarea[ng-model="$ctrl.note.text"]',
@ -312,15 +306,6 @@ export default {
clientMandate: {
firstMandateText: 'vn-client-mandate vn-card vn-table vn-tbody > vn-tr'
},
clientLog: {
lastModificationPreviousValue: 'vn-client-log vn-tr table tr td.before',
lastModificationCurrentValue: 'vn-client-log vn-tr table tr td.after',
namePreviousValue: 'vn-client-log vn-tr table tr:nth-child(1) td.before',
nameCurrentValue: 'vn-client-log vn-tr table tr:nth-child(1) td.after',
activePreviousValue: 'vn-client-log vn-tr:nth-child(2) table tr:nth-child(2) td.before',
activeCurrentValue: 'vn-client-log vn-tr:nth-child(2) table tr:nth-child(2) td.after'
},
clientBalance: {
company: 'vn-client-balance-index vn-autocomplete[ng-model="$ctrl.companyId"]',
newPaymentButton: `vn-float-button`,
@ -755,6 +740,7 @@ export default {
anyDocument: 'vn-ticket-dms-index > vn-data-viewer vn-tbody vn-tr'
},
ticketFuture: {
searchResult: 'vn-ticket-future tbody tr',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
originDated: 'vn-date-picker[label="Origin date"]',
futureDated: 'vn-date-picker[label="Destination date"]',
@ -770,7 +756,6 @@ export default {
problems: 'vn-check[label="With problems"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Future tickets"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(1) > td > vn-check',
multiCheck: 'vn-multi-check',
tableId: 'vn-textfield[name="id"]',
@ -1361,18 +1346,6 @@ export default {
notes: 'vn-supplier-basic-data vn-textarea[ng-model="$ctrl.supplier.note"]',
saveButton: 'vn-supplier-basic-data button[type="submit"]',
},
supplierFiscalData: {
socialName: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.name"]',
taxNumber: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.nif"]',
account: 'vn-supplier-fiscal-data vn-textfield[ng-model="$ctrl.supplier.account"]',
sageTaxType: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageTaxTypeFk"]',
sageWihholding: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.sageWithholdingFk"]',
postCode: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.postCode"]',
city: 'vn-supplier-fiscal-data vn-datalist[ng-model="$ctrl.supplier.city"]',
province: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.provinceFk"]',
country: 'vn-supplier-fiscal-data vn-autocomplete[ng-model="$ctrl.supplier.countryFk"]',
saveButton: 'vn-supplier-fiscal-data button[type="submit"]',
},
supplierBillingData: {
payMethod: 'vn-supplier-billing-data vn-autocomplete[ng-model="$ctrl.supplier.payMethodFk"]',
payDem: 'vn-supplier-billing-data vn-autocomplete[ng-model="$ctrl.supplier.payDemFk"]',

View File

@ -79,27 +79,17 @@ describe('SmartTable SearchBar integration', () => {
it('should order by first id', async() => {
await page.loginAndModule('developer', 'item');
await page.accessToSection('item.fixedPrice');
await page.doSearch();
await page.keyboard.press('Enter');
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('1');
await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '1');
});
it('should order by last id', async() => {
it('should order by last id, reload page and have same order', async() => {
await page.waitToClick(selectors.itemFixedPrice.orderColumnId);
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('3');
});
it('should reload page and have same order', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('3');
await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '13');
});
});
});

View File

@ -1,88 +1,56 @@
/* eslint max-len: ["error", { "code": 150 }]*/
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Client Edit web access path', () => {
const $ = {
enableWebAccess: 'vn-client-web-access vn-check[label="Enable web access"]',
userName: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.name"]',
email: 'vn-client-web-access vn-textfield[ng-model="$ctrl.account.email"]',
saveButton: 'vn-client-web-access button[type=submit]',
nameValue: 'vn-client-log .change:nth-child(1) .basic-json:nth-child(1) vn-json-value',
activeValue: 'vn-client-log .change:nth-child(2) .basic-json:nth-child(2) vn-json-value'
};
describe('Client web access path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('salesPerson', 'client');
await page.accessToSearchResult('max');
await page.accessToSection('client.card.webAccess');
});
afterAll(async() => {
await browser.close();
});
it('should uncheck the Enable web access checkbox', async() => {
await page.waitToClick(selectors.clientWebAccess.enableWebAccessCheckbox);
await page.waitToClick(selectors.clientWebAccess.saveButton);
const message = await page.waitForSnackbar();
it('should modify and save web access attributes', async() => {
await page.accessToSection('client.card.webAccess');
await page.click($.enableWebAccess);
await page.click($.saveButton);
const enableMessage = await page.waitForSnackbar();
await page.overwrite($.userName, 'Legion');
await page.overwrite($.email, 'legion@marvel.com');
await page.click($.saveButton);
const modifyMessage = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should update the name`, async() => {
await page.clearInput(selectors.clientWebAccess.userName);
await page.write(selectors.clientWebAccess.userName, 'Legion');
await page.waitToClick(selectors.clientWebAccess.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should update the email`, async() => {
await page.clearInput(selectors.clientWebAccess.email);
await page.write(selectors.clientWebAccess.email, 'legion@marvel.com');
await page.waitToClick(selectors.clientWebAccess.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should reload the section and confirm web access is now unchecked', async() => {
await page.reloadSection('client.card.webAccess');
const result = await page.checkboxState(selectors.clientWebAccess.enableWebAccessCheckbox);
const hasAccess = await page.checkboxState($.enableWebAccess);
const userName = await page.getValue($.userName);
const email = await page.getValue($.email);
expect(result).toBe('unchecked');
});
it('should confirm web access name have been updated', async() => {
const result = await page.waitToGetProperty(selectors.clientWebAccess.userName, 'value');
expect(result).toEqual('Legion');
});
it('should confirm web access email have been updated', async() => {
const result = await page.waitToGetProperty(selectors.clientWebAccess.email, 'value');
expect(result).toEqual('legion@marvel.com');
});
it(`should navigate to the log section`, async() => {
await page.accessToSection('client.card.log');
});
const logName = await page.innerText($.nameValue);
const logActive = await page.innerText($.activeValue);
it(`should confirm the last log shows the updated client name and no modifications on active checkbox`, async() => {
let namePreviousValue = await page
.waitToGetProperty(selectors.clientLog.namePreviousValue, 'innerText');
let nameCurrentValue = await page
.waitToGetProperty(selectors.clientLog.nameCurrentValue, 'innerText');
expect(enableMessage.type).toBe('success');
expect(modifyMessage.type).toBe('success');
expect(namePreviousValue).toEqual('MaxEisenhardt');
expect(nameCurrentValue).toEqual('Legion');
});
expect(hasAccess).toBe('unchecked');
expect(userName).toEqual('Legion');
expect(email).toEqual('legion@marvel.com');
it(`should confirm the penultimate log shows the updated active and no modifications on client name`, async() => {
let activePreviousValue = await page
.waitToGetProperty(selectors.clientLog.activePreviousValue, 'innerText');
let activeCurrentValue = await page
.waitToGetProperty(selectors.clientLog.activeCurrentValue, 'innerText');
expect(activePreviousValue).toEqual('✓');
expect(activeCurrentValue).toEqual('✗');
expect(logName).toEqual('Legion');
expect(logActive).toEqual('✗');
});
});

View File

@ -246,6 +246,7 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);

View File

@ -126,10 +126,11 @@ describe('Ticket Future path', () => {
});
it('should check the three last tickets and move to the future', async() => {
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.ticketFuture.acceptButton);
await page.waitToClick(selectors.globalItems.acceptButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');

View File

@ -35,7 +35,7 @@ describe('InvoiceIn serial path', () => {
});
it('should go to index and check if the search-panel has the correct params', async() => {
await page.click(selectors.invoiceInSerial.goToIndex);
await page.waitToClick(selectors.invoiceInSerial.goToIndex);
const params = await page.$$(selectors.invoiceInIndex.topbarSearchParams);
const serial = await params[0].getProperty('title');
const isBooked = await params[1].getProperty('title');

View File

@ -1,6 +1,6 @@
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn negative bases path', () => {
describe('InvoiceOut negative bases path', () => {
let browser;
let page;
const httpRequests = [];
@ -9,11 +9,11 @@ describe('InvoiceIn negative bases path', () => {
browser = await getBrowser();
page = browser.page;
page.on('request', req => {
if (req.url().includes(`InvoiceIns/negativeBases`))
if (req.url().includes(`InvoiceOuts/negativeBases`))
httpRequests.push(req.url());
});
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSection('invoiceIn.negative-bases');
await page.loginAndModule('administrative', 'invoiceOut');
await page.accessToSection('invoiceOut.negative-bases');
});
afterAll(async() => {

View File

@ -20,7 +20,6 @@ describe('Entry basic data path', () => {
it('should edit the basic data', async() => {
await page.write(selectors.entryBasicData.reference, 'new movement 8');
await page.write(selectors.entryBasicData.invoiceNumber, 'new movement 8');
await page.write(selectors.entryBasicData.notes, 'new notes');
await page.write(selectors.entryBasicData.observations, ' edited');
await page.autocompleteSearch(selectors.entryBasicData.supplier, 'Plants nick');
await page.autocompleteSearch(selectors.entryBasicData.currency, 'eur');
@ -53,12 +52,6 @@ describe('Entry basic data path', () => {
expect(result).toEqual('new movement 8');
});
it('should confirm the note was edited', async() => {
const result = await page.waitToGetProperty(selectors.entryBasicData.notes, 'value');
expect(result).toEqual('new notes');
});
it('should confirm the observation was edited', async() => {
const result = await page.waitToGetProperty(selectors.entryBasicData.observations, 'value');

View File

@ -1,6 +1,20 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
const $ = {
saveButton: 'vn-supplier-fiscal-data button[type="submit"]',
};
const $inputs = {
province: 'vn-supplier-fiscal-data [name="province"]',
country: 'vn-supplier-fiscal-data [name="country"]',
postcode: 'vn-supplier-fiscal-data [name="postcode"]',
city: 'vn-supplier-fiscal-data [name="city"]',
socialName: 'vn-supplier-fiscal-data [name="socialName"]',
taxNumber: 'vn-supplier-fiscal-data [name="taxNumber"]',
account: 'vn-supplier-fiscal-data [name="account"]',
sageWithholding: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageWithholdingFk"]',
sageTaxType: 'vn-supplier-fiscal-data [ng-model="$ctrl.supplier.sageTaxTypeFk"]'
};
describe('Supplier fiscal data path', () => {
let browser;
let page;
@ -10,102 +24,44 @@ describe('Supplier fiscal data path', () => {
page = browser.page;
await page.loginAndModule('administrative', 'supplier');
await page.accessToSearchResult('2');
await page.accessToSection('supplier.card.fiscalData');
});
afterAll(async() => {
await browser.close();
});
it('should attempt to edit the fiscal data but fail as the tax number is invalid', async() => {
await page.clearInput(selectors.supplierFiscalData.city);
await page.clearInput(selectors.supplierFiscalData.province);
await page.clearInput(selectors.supplierFiscalData.country);
await page.clearInput(selectors.supplierFiscalData.postCode);
await page.write(selectors.supplierFiscalData.city, 'Valencia');
await page.waitForTimeout(1000); // must repeat this action twice or fails. also #2699 may be a cool solution to this.
await page.clearInput(selectors.supplierFiscalData.city);
await page.write(selectors.supplierFiscalData.city, 'Valencia');
await page.clearInput(selectors.supplierFiscalData.socialName);
await page.write(selectors.supplierFiscalData.socialName, 'Farmer King SL');
await page.clearInput(selectors.supplierFiscalData.taxNumber);
await page.write(selectors.supplierFiscalData.taxNumber, 'Wrong tax number');
await page.clearInput(selectors.supplierFiscalData.account);
await page.write(selectors.supplierFiscalData.account, '0123456789');
await page.autocompleteSearch(selectors.supplierFiscalData.sageWihholding, 'retencion estimacion objetiva');
await page.autocompleteSearch(selectors.supplierFiscalData.sageTaxType, 'operaciones no sujetas');
it('should attempt to edit the fiscal data and check data is saved', async() => {
await page.accessToSection('supplier.card.fiscalData');
await page.clearInput($inputs.province);
await page.clearInput($inputs.country);
await page.clearInput($inputs.postcode);
await page.overwrite($inputs.city, 'Valencia');
await page.overwrite($inputs.socialName, 'Farmer King SL');
await page.overwrite($inputs.taxNumber, 'Wrong tax number');
await page.overwrite($inputs.account, '0123456789');
await page.autocompleteSearch($inputs.sageWithholding, 'retencion estimacion objetiva');
await page.autocompleteSearch($inputs.sageTaxType, 'operaciones no sujetas');
await page.click($.saveButton);
const errorMessage = await page.waitForSnackbar();
await page.overwrite($inputs.taxNumber, '12345678Z');
await page.click($.saveButton);
const successMessage = await page.waitForSnackbar();
await page.waitToClick(selectors.supplierFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Invalid Tax number');
});
it('should save the changes as the tax number is valid this time', async() => {
await page.clearInput(selectors.supplierFiscalData.taxNumber);
await page.write(selectors.supplierFiscalData.taxNumber, '12345678Z');
await page.waitToClick(selectors.supplierFiscalData.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should reload the section', async() => {
await page.reloadSection('supplier.card.fiscalData');
const values = await page.getValues($inputs);
expect(errorMessage.text).toContain('Invalid Tax number');
expect(successMessage.type).toBe('success');
expect(values).toEqual({
province: 'Province one (España)',
country: 'España',
postcode: '46000',
city: 'Valencia',
socialName: 'Farmer King SL',
taxNumber: '12345678Z',
account: '0123456789',
sageWithholding: 'RETENCION ESTIMACION OBJETIVA',
sageTaxType: 'Operaciones no sujetas'
});
it('should check the socialName was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.socialName, 'value');
expect(result).toEqual('Farmer King SL');
});
it('should check the taxNumber was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.taxNumber, 'value');
expect(result).toEqual('12345678Z');
});
it('should check the account was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.account, 'value');
expect(result).toEqual('0123456789');
});
it('should check the sageWihholding was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageWihholding, 'value');
expect(result).toEqual('RETENCION ESTIMACION OBJETIVA');
});
it('should check the sageTaxType was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.sageTaxType, 'value');
expect(result).toEqual('Operaciones no sujetas');
});
it('should check the postCode was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.postCode, 'value');
expect(result).toEqual('46000');
});
it('should check the city was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.city, 'value');
expect(result).toEqual('Valencia');
});
it('should check the province was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.province, 'value');
expect(result).toEqual('Province one (España)');
});
it('should check the country was edited', async() => {
const result = await page.waitToGetProperty(selectors.supplierFiscalData.country, 'value');
expect(result).toEqual('España');
});
});

View File

@ -174,7 +174,6 @@ export default class Autocomplete extends Field {
refreshDisplayed() {
let display = '';
let hasTemplate = this.$transclude && this.$transclude.isSlotFilled('tplItem');
if (this._selection && this.showField) {
if (this.multiple && Array.isArray(this._selection)) {
@ -182,19 +181,8 @@ export default class Autocomplete extends Field {
if (display.length > 0) display += ', ';
display += item[this.showField];
}
} else {
} else
display = this._selection[this.showField];
if (hasTemplate) {
let template = this.$transclude(() => {}, null, 'tplItem');
const element = template[0];
const description = element.querySelector('.text-secondary');
if (description) description.remove();
const displayElement = angular.element(element);
const displayText = displayElement.text();
display = this.$interpolate(displayText)(this._selection);
}
}
}
this.input.value = display;

View File

@ -0,0 +1,5 @@
<div class="letter">
{{::$ctrl.val && $ctrl.val.charAt(0).toUpperCase()}}
</div>
<div class="image" ng-transclude>
</div>

View File

@ -0,0 +1,63 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
/**
* Displays colored avatar based on value.
*
* @property {*} val The value
*/
export default class Avatar extends Component {
get val() {
return this._val;
}
set val(value) {
this._val = value;
const val = value || '';
let hash = 0;
for (let i = 0; i < val.length; i++)
hash += val.charCodeAt(i);
const color = '#' + colors[hash % colors.length];
const el = this.element;
el.style.backgroundColor = color;
el.title = val;
}
}
ngModule.vnComponent('vnAvatar', {
template: require('./index.html'),
controller: Avatar,
bindings: {
val: '@?'
},
transclude: true
});
const colors = [
'e2553d', // Coral
'FFA07A', // Salmon
'FFDAB9', // Peach
'a17077', // Pink
'bf0e99', // Pink light
'52a500', // Green chartreuse
'00aeae', // Cian
'b754cf', // Purple middle
'8a69cd', // Blue lavender
'1fa8a1', // Green ocean
'DC143C', // Red crimson
'5681cf', // Blue steel
'FF1493', // Ping intense
'02ba02', // Green lime
'1E90FF', // Blue sky
'8B008B', // Purple dark
'cc7000', // Orange bright
'00b5b8', // Turquoise
'8B0000', // Red dark
'008080', // Green bluish
'2F4F4F', // Gray board
'7e7e7e', // Gray
'5d5d5d', // Gray dark
];

View File

@ -0,0 +1,32 @@
@import "variables";
vn-avatar {
display: block;
border-radius: 50%;
overflow: hidden;
height: 36px;
width: 36px;
font-size: 22px;
background-color: $color-main;
position: relative;
& > * {
width: 100%;
height: 100%;
}
& > .letter {
display: flex;
align-items: center;
justify-content: center;
}
& > .image {
position: absolute;
top: 0;
left: 0;
& > img {
width: 100%;
height: 100%;
}
}
}

View File

@ -166,7 +166,7 @@ export default class Field extends FormInput {
if (event.defaultPrevented) return;
event.preventDefault();
this.field = null;
this.input.dispatchEvent(new Event('change'));
this.element.dispatchEvent(new Event('change'));
}
buildInput(type) {

View File

@ -17,6 +17,7 @@ import './pagination/pagination';
import './searchbar/searchbar';
import './scroll-up/scroll-up';
import './autocomplete';
import './avatar';
import './button';
import './button-menu';
import './calendar';
@ -32,6 +33,7 @@ import './float-button';
import './icon-menu';
import './icon-button';
import './input-number';
import './json-value';
import './label-value';
import './range';
import './input-time';

View File

@ -0,0 +1,73 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
const maxStrLen = 50;
/**
* Displays pretty JSON value.
*
* @property {*} value The value
*/
export default class Controller extends Component {
get value() {
return this._value;
}
set value(value) {
const wasEmpty = this._value === undefined;
this._value = value;
let text;
let cssClass;
const type = typeof value;
if (value == null) {
text = '∅';
cssClass = 'null';
} else {
cssClass = type;
switch (type) {
case 'boolean':
text = value ? '✓' : '✗';
cssClass = value ? 'true' : 'false';
break;
case 'string':
text = value.length <= maxStrLen
? value
: value.substring(0, maxStrLen) + '...';
break;
case 'object':
if (value instanceof Date) {
const hasZeroTime =
value.getHours() === 0 &&
value.getMinutes() === 0 &&
value.getSeconds() === 0;
const format = hasZeroTime ? 'dd/MM/yyyy' : 'dd/MM/yyyy HH:mm:ss';
text = this.$filter('date')(value, format);
} else
text = value;
break;
default:
text = value;
}
}
const el = this.element;
el.textContent = text;
el.title = type == 'string' && value.length > maxStrLen ? value : '';
cssClass = `json-${cssClass}`;
if (wasEmpty)
el.classList.add(cssClass);
else
el.classList.replace(this.className, cssClass);
}
}
ngModule.vnComponent('vnJsonValue', {
controller: Controller,
bindings: {
value: '<?'
}
});

View File

@ -0,0 +1,79 @@
import './index';
describe('Component vnJsonValue', () => {
let controller;
let $scope;
let $element;
let el;
beforeEach(ngModule('vnCore'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$element = angular.element('<vn-json-value></vn-json-value>');
controller = $componentController('vnJsonValue', {$element, $scope});
el = controller.element;
}));
describe('set value()', () => {
it('should display null symbol when value is null equivalent', () => {
controller.value = null;
expect(el.textContent).toEqual('∅');
expect(el.className).toContain('json-null');
});
it('should display ballot when value is false', () => {
controller.value = false;
expect(el.textContent).toEqual('✗');
expect(el.className).toContain('json-false');
});
it('should display check when value is true', () => {
controller.value = true;
expect(el.textContent).toEqual('✓');
expect(el.className).toContain('json-true');
});
it('should display string when value is an string', () => {
controller.value = 'Foo';
expect(el.textContent).toEqual('Foo');
expect(el.className).toContain('json-string');
});
it('should display only date when value is date with time set to zero', () => {
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
controller.value = date;
expect(el.textContent).toEqual('01/01/2001');
expect(el.className).toContain('json-object');
});
it('should display full date without time when value is date with time', () => {
const date = Date.vnNew();
date.setHours(15, 45);
controller.value = date;
expect(el.textContent).toEqual('01/01/2001 15:45:00');
expect(el.className).toContain('json-object');
});
it('should display object when value is an object', () => {
controller.value = {foo: 'bar'};
expect(el.textContent).toEqual('[object Object]');
expect(el.className).toContain('json-object');
});
it('should display number when value is a number', () => {
controller.value = 2050;
expect(el.textContent).toEqual('2050');
expect(el.className).toContain('json-number');
});
});
});

View File

@ -0,0 +1,23 @@
vn-json-value {
display: inline;
&.json-string {
color: #d172cc;
}
&.json-object {
color: #d1a572;
}
&.json-number {
color: #85d0ff;
}
&.json-true {
color: #7dc489;
}
&.json-false {
color: #c74949;
}
&.json-null {
color: #cd7c7c;
font-style: italic;
}
}

View File

@ -41,10 +41,15 @@ vn-table {
display: table-row;
height: 48px;
}
vn-thead, .vn-thead,
vn-tbody, .vn-tbody,
vn-tfoot, .vn-tfoot,
thead, tbody, tfoot {
& > thead,
& > tbody,
& > tfoot,
& > vn-thead,
& > vn-tbody,
& > vn-tfoot,
& > .vn-thead,
& > .vn-tbody,
& > .vn-tfoot {
& > * {
display: table-row;
@ -111,14 +116,14 @@ vn-table {
color: inherit;
}
}
a.vn-tbody {
& > a.vn-tbody {
&.clickable {
@extend %clickable;
}
}
vn-tbody > *,
.vn-tbody > *,
tbody > * {
& > vn-tbody > *,
& > .vn-tbody > *,
& > tbody > * {
border-bottom: $border-thin;
&:last-child {

View File

@ -2,7 +2,6 @@
$font-size: 11pt;
$menu-width: 256px;
$right-menu-width: 318px;
$topbar-height: 56px;
$mobile-width: 800px;
$float-spacing: 20px;

View File

@ -88,13 +88,13 @@ vn-layout {
}
&.right-menu {
& > vn-topbar > .end {
width: 80px + $right-menu-width;
width: 80px + $menu-width;
}
& > .main-view {
padding-right: $right-menu-width;
padding-right: $menu-width;
}
[fixed-bottom-right] {
right: $right-menu-width;
right: $menu-width;
}
}
& > .main-view {

View File

@ -3,71 +3,212 @@
url="{{$ctrl.url}}"
filter="$ctrl.filter"
link="{originFk: $ctrl.originId}"
where="{changedModel: $ctrl.changedModel,
changedModelId: $ctrl.changedModelId}"
where="{changedModel: $ctrl.changedModel, changedModelId: $ctrl.changedModelId}"
data="$ctrl.logs"
limit="20"
order="creationDate DESC, id DESC"
limit="20">
</vn-crud-model>
<vn-crud-model
url="{{$ctrl.url}}/{{$ctrl.originId}}/models"
data="models"
order="changedModel"
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model" class="vn-w-xl">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="creationDate">Date</vn-th>
<vn-th field="userFk" shrink>User</vn-th>
<vn-th field="changedModel" ng-if="$ctrl.showModelName" shrink>Model</vn-th>
<vn-th field="action" shrink>Action</vn-th>
<vn-th field="changedModelValue" ng-if="$ctrl.showModelName">Name</vn-th>
<vn-th expand>Changes</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="log in $ctrl.logs">
<vn-td shrink-datetime>
{{::log.creationDate | date:'dd/MM/yyyy HH:mm'}}
</vn-td>
<vn-td>
<span ng-class="{'link': log.user.worker.id, 'value': !log.user.worker.id}"
ng-click="$ctrl.showWorkerDescriptor($event, log.user.worker.id)"
translate>{{::log.user.name || 'System' | translate}}
</span>
</vn-td>
<vn-td ng-if="$ctrl.showModelName">
{{::log.changedModel}}
</vn-td>
<vn-td shrink translate>
{{::$ctrl.actionsText[log.action]}}
</vn-td>
<vn-td ng-if="$ctrl.showModelName">
{{::log.changedModelValue}}
</vn-td>
<vn-td expand>
<table class="attributes">
<thead>
<tr>
<th translate class="field">Field</th>
<th translate>Before</th>
<th translate>After</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="prop in ::log.props">
<td class="field">{{prop.name}}</td>
<td class="before">{{prop.old}}</td>
<td class="after">{{prop.new}}</td>
</tr>
</tbody>
</table>
<div ng-if="log.description != null">
{{::log.description}}
<vn-data-viewer model="model" class="vn-w-md vn-px-sm">
<div class="change vn-mb-sm" ng-repeat="log in $ctrl.logs">
<div class="user-wrapper">
<vn-avatar class="vn-mt-xs"
ng-class="::{system: !log.user}"
val="{{::log.user ? log.user.nickname : 'System'}}"
ng-click="$ctrl.showWorkerDescriptor($event, log)">
<img
ng-if="::log.user.image"
ng-src="/api/Images/user/160x160/{{::log.userFk}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
<div class="arrow bg-panel"></div>
<div class="line"></div>
</div>
<vn-card class="detail vn-pa-sm">
<div class="header vn-mb-sm">
<div
class="date text-secondary text-caption"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<span class="chip" ng-class="::$ctrl.actionsClass[log.action]" translate>
{{::$ctrl.actionsText[log.action]}}
</span>
</div>
<div
class="model vn-mb-sm"
title="{{::log.changedModelValue}}"
ng-if="::log.changedModel || log.changedModelValue">
<span class="model-name"
ng-if="::$ctrl.showModelName"
title="{{::log.changedModel}}">
{{::log.changedModelI18n}}
</span>
<span class="model-id"
ng-if="::log.changedModelId">
#{{::log.changedModelId}}
</span>
<span class="model-value">
{{::log.changedModelValue}}
</span>
</div>
<div class="changes"
ng-class="::log.props.length ? 'props' : 'no-props'"
vn-id="changes">
<vn-icon icon="visibility"
class="expand-button"
ng-click="$ctrl.toggleAttributes(log, changes, true)">
</vn-icon>
<vn-icon icon="visibility_off"
class="shrink-button"
ng-click="$ctrl.toggleAttributes(log, changes, false)">
</vn-icon>
<div class="changes-wrapper">
<span ng-if="::log.props.length"
class="attributes">
<span ng-if="!log.expand" ng-repeat="prop in ::log.props"
class="basic-json">
<span class="json-field"
title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand"
class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field"
title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value>
<span ng-if="::log.action == 'update'">
<vn-json-value value="::prop.old"></vn-json-value>
</span>
</div>
</div>
</span>
<span ng-if="::!log.props.length"
class="description">
{{::log.description}}
</span>
<span ng-if="::!log.description && !log.props.length"
class="no-changes"
translate>
No changes
</span>
</div>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</div>
</div>
</vn-data-viewer>
<vn-side-menu side="right">
<form vn-vertical
ng-model-options="{updateOn: 'change blur'}"
class="vn-pa-md filter">
<vn-textfield
label="Name"
ng-model="filter.changedModelValue">
</vn-textfield>
<vn-vertical>
<vn-radio
label="All"
val="all"
ng-model="filter.who">
</vn-radio>
<vn-radio
label="User"
val="user"
ng-model="filter.who">
</vn-radio>
<vn-radio
label="System"
val="system"
ng-model="filter.who">
</vn-radio>
</div>
<vn-autocomplete
ng-show="filter.who != 'system'"
label="User"
ng-model="filter.userFk"
value-field="id"
show-field="nickname"
fields="['id', 'name', 'nickname', 'image']"
search-function="$ctrl.searchUser($search)"
url="{{$ctrl.url}}/{{$ctrl.originId}}/editors"
order="nickname">
<tpl-item>
<div style="display: flex;">
<vn-avatar
class="vn-mr-sm"
val="{{::nickname}}">
<img
ng-if="::image"
ng-src="/api/Images/user/160x160/{{::id}}/download?access_token={{::$ctrl.vnToken.token}}">
</img>
</vn-avatar>
<div>
<div>{{::nickname}}</div>
<div class="text-secondary text-caption">{{::name}}</div>
</div>
</div>
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
label="Model"
ng-model="filter.changedModel"
value-field="changedModel"
show-field="changedModel"
data="models">
</vn-autocomplete>
<vn-textfield
label="Id"
ng-model="filter.changedModelId">
</vn-textfield>
<vn-vertical>
<vn-check
label="Creates"
ng-model="filter.actions.insert">
</vn-check>
<vn-check
label="Updates"
ng-model="filter.actions.update">
</vn-check>
<vn-check
label="Deletes"
ng-model="filter.actions.delete">
</vn-check>
<vn-check
label="Views"
ng-model="filter.actions.select">
</vn-check>
</div>
<vn-date-picker
label="Date"
ng-model="filter.from">
</vn-date-picker>
<vn-date-picker
label="To"
ng-model="filter.to">
</vn-date-picker>
<vn-button-bar vn-vertical class="vn-mt-sm">
<vn-button
label="Filter"
ng-click="$ctrl.applyFilter(filter)">
</vn-button>
<vn-button
label="Reset"
class="flat"
ng-click="$ctrl.resetFilter()"
ng-if="model.userFilter">
</vn-button>
</vn-button-bar>
</form>
</vn-side-menu>
<vn-worker-descriptor-popover vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -13,11 +13,17 @@ export default class Controller extends Section {
delete: 'Deletes',
select: 'Views'
};
this.actionsClass = {
insert: 'success',
update: 'warning',
delete: 'alert',
select: 'notice'
};
this.filter = {
include: [{
relation: 'user',
scope: {
fields: ['name'],
fields: ['nickname', 'name', 'image'],
include: {
relation: 'worker',
scope: {
@ -27,6 +33,20 @@ export default class Controller extends Section {
},
}],
};
this.dateFilter = this.$filter('date');
this.lang = this.$translate.use();
this.today = Date.vnNew();
this.today.setHours(0, 0, 0, 0);
}
$postLink() {
this.resetFilter();
this.$.$watch(
() => this.$.filter,
() => this.applyFilter(),
true
);
}
get logs() {
@ -42,6 +62,7 @@ export default class Controller extends Section {
const oldValues = log.oldInstance || empty;
const newValues = log.newInstance || empty;
const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = locale.name || log.changedModel;
let props = Object.keys(oldValues).concat(Object.keys(newValues));
props = [...new Set(props)];
@ -49,9 +70,10 @@ export default class Controller extends Section {
log.props = [];
for (const prop of props) {
log.props.push({
name: locale[prop] || prop,
old: this.formatValue(oldValues[prop]),
new: this.formatValue(newValues[prop])
name: prop,
nameI18n: locale.columns?.[prop] || prop,
old: this.castJsonValue(oldValues[prop]),
new: this.castJsonValue(newValues[prop])
});
}
}
@ -61,36 +83,112 @@ export default class Controller extends Section {
return !(this.changedModel && this.changedModelId);
}
formatValue(value) {
let type = typeof value;
if (type === 'string' && validDate.test(value)) {
value = new Date(value);
type = typeof value;
castJsonValue(value) {
return typeof value === 'string' && validDate.test(value)
? new Date(value)
: value;
}
switch (type) {
case 'boolean':
return value ? '✓' : '✗';
case 'object':
if (value instanceof Date) {
const hasZeroTime =
value.getHours() === 0 &&
value.getMinutes() === 0 &&
value.getSeconds() === 0;
const format = hasZeroTime ? 'dd/MM/yyyy' : 'dd/MM/yyyy HH:mm:ss';
return this.$filter('date')(value, format);
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
toggleAttributes(log, changesEl, force) {
log.expand = force;
changesEl.classList.toggle('expanded', force);
}
relativeDate(dateVal) {
if (dateVal == null) return '';
const date = new Date(dateVal);
const dateZeroTime = new Date(dateVal);
dateZeroTime.setHours(0, 0, 0, 0);
const diff = Math.trunc((this.today.getTime() - dateZeroTime.getTime()) / (1000 * 3600 * 24));
let format;
if (diff == 0)
format = `'${this.$t('today')}'`;
else if (diff == 1)
format = `'${this.$t('yesterday')}'`;
else if (diff > 1 && diff < 7)
format = `'${date.toLocaleDateString(this.lang, {weekday: 'short'})}'`;
else if (this.today.getFullYear() == date.getFullYear())
format = `d '${date.toLocaleDateString(this.lang, {month: 'short'})}'`;
else
return value;
format = `dd/MM/yyyy`;
return this.dateFilter(date, `${format} HH:mm`);
}
resetFilter() {
this.$.filter = {who: 'all'};
}
applyFilter() {
const filter = this.$.filter;
function getParam(prop, value) {
if (value == null || value == '') return null;
switch (prop) {
case 'changedModelValue':
return {[prop]: {like: `%${value}%`}};
case 'who':
switch (value) {
case 'all':
return null;
case 'user':
return {userFk: {neq: null}};
case 'system':
return {userFk: null};
}
case 'actions':
const inq = [];
for (const action in value) {
if (value[action])
inq.push(action);
}
return inq.length ? {action: {inq}} : null;
case 'from':
if (filter.to) {
return {creationDate: {gte: value}};
} else {
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {between: [value, to]}};
}
case 'to':
const to = new Date(value);
to.setHours(23, 59, 59, 999);
return {creationDate: {lte: to}};
default:
return value;
return {[prop]: value};
}
}
showWorkerDescriptor(event, workerId) {
if (!workerId) return;
this.$.workerDescriptor.show(event.target, workerId);
const and = [];
for (const prop in filter) {
const param = getParam(prop, filter[prop]);
if (param) and.push(param);
}
const lbFilter = and.length ? {where: {and}} : null;
return this.$.model.applyFilter(lbFilter);
}
searchUser(search) {
if (/^[0-9]+$/.test(search)) {
return {id: search};
} else {
return {or: [
{name: search},
{nickname: {like: `%${search}%`}}
]}
}
}
showWorkerDescriptor(event, log) {
if (log.user?.worker)
this.$.workerDescriptor.show(event.target, log.userFk);
}
}

View File

@ -0,0 +1,97 @@
import './index';
describe('Salix Component vnLog', () => {
let controller;
let $scope;
let $element;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
$element = angular.element('<vn-log></vn-log>');
controller = $componentController('vnLog', {$element, $scope});
}));
describe('relativeDate()', () => {
let date;
beforeEach(() => {
date = Date.vnNew();
});
it('should return empty string when date is null', () => {
const ret = controller.relativeDate(null);
expect(ret).toEqual('');
});
it('should return empty string when date is undefined', () => {
const ret = controller.relativeDate(undefined);
expect(ret).toEqual('');
});
it('should return today and time when date is today', () => {
const ret = controller.relativeDate(date);
expect(ret).toEqual('today 12:00');
});
it('should return yesterday and time when date is yesterday', () => {
date.setDate(date.getDate() - 1);
const ret = controller.relativeDate(date);
expect(ret).toEqual('yesterday 12:00');
});
it('should return abreviated weekday name and time when date is on past week', () => {
date.setDate(date.getDate() - 3);
const ret = controller.relativeDate(date);
expect(ret).toEqual('Fri 12:00');
});
it('should return abreviated month name, day number and time when date is on this year', () => {
date.setDate(date.getDate() + 20);
const ret = controller.relativeDate(date);
expect(ret).toEqual('21 Jan 12:00');
});
it('should return abreviated month name, day number, year and time when date is on different year', () => {
date.setDate(date.getDate() - 20);
const ret = controller.relativeDate(date);
expect(ret).toEqual('12/12/2000 12:00');
});
it('should convert to date and return string when date is not a Date class instance', () => {
const ret = controller.relativeDate(date.toJSON());
expect(ret).toEqual('today 12:00');
});
});
describe('castJsonValue()', () => {
it('should return date when string has valid JSON date format', () => {
const now = Date.vnNew();
const ret = controller.castJsonValue(now.toJSON());
expect(ret).toBeInstanceOf(Date);
});
it('should return same value when is string with invalid JSON date format', () => {
const ret = controller.castJsonValue('Foo');
expect(ret).toEqual('Foo');
});
it('should return same value when is not an string', () => {
const ret = controller.castJsonValue(1001);
expect(ret).toEqual(1001);
});
});
});

View File

@ -13,3 +13,6 @@ Views: Visualiza
System: Sistema
note: nota
Changes: Cambios
No changes: No hay cambios
today: hoy
yesterday: ayer

View File

@ -1,66 +1,152 @@
@import "variables";
vn-log {
vn-td {
vertical-align: initial !important;
}
.changes {
display: none;
}
.label {
color: $color-font-secondary;
}
.value {
color: $color-font;
}
.change {
display: flex;
@media screen and (max-width: 1570px) {
vn-table .expendable {
& > .user-wrapper {
position: relative;
padding-right: 10px;
& > vn-avatar {
cursor: pointer;
&.system {
background-color: $color-main !important;
}
}
& > .arrow {
height: 8px;
width: 8px;
position: absolute;
transform: rotateY(0deg) rotate(45deg);
top: 18px;
right: -4px;
z-index: 1;
}
& > .line {
position: absolute;
background-color: $color-main;
width: 2px;
left: 17px;
z-index: -1;
top: 44px;
bottom: -8px;
}
}
&:last-child > .user-wrapper > .line {
display: none;
}
.detail {
position: relative;
flex-grow: 1;
width: 100%;
border-radius: 2px;
overflow: hidden;
& > .header {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& > .chip {
padding: 2px 4px;
border-radius: 4px;
display: inline-block;
color: $color-font-bg;
&.notice {
background-color: $color-notice-medium;
}
&.success {
background-color: $color-success-medium;
}
&.warning {
background-color: $color-main-medium;
}
&.alert {
background-color: lighten($color-alert, 5%);
}
}
.date {
float: right;
}
}
& > .model {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
& > .model-name {
text-transform: capitalize;
}
& > .model-value {
font-style: italic;
color: #c7bd2b;
}
& > .model-id {
color: $color-font-secondary;
font-size: .9em;
}
}
}
}
.changes {
padding-top: 10px;
overflow: hidden;
background-color: rgba(255, 255, 255, .05);
border-radius: 4px;
color: $color-font-secondary;
transition: max-height 150ms ease-in-out;
max-height: 28px;
position: relative;
& > .expand-button,
& > .shrink-button {
display: none;
}
&.props {
padding-right: 24px;
& > .expand-button,
& > .shrink-button {
position: absolute;
top: 6px;
right: 8px;
font-size: inherit;
float: right;
cursor: pointer;
}
& > .expand-button {
display: block;
}
}
.attributes {
width: 100%;
&.expanded {
max-height: 500px;
padding-right: 0;
tr {
height: 10px;
& > td {
padding: 2px;
& > .changes-wrapper {
text-overflow: initial;
white-space: initial;
}
& > td.field,
& > th.field {
width: 20%;
color: gray;
& > .shrink-button {
display: block;
}
& > td.before,
& > th.before,
& > td.after,
& > th.after {
width: 40%;
white-space: pre-line;
& > .expand-button {
display: none;
}
}
}
}
.ellipsis {
white-space: nowrap;
& > .changes-wrapper {
padding: 4px 6px;
overflow: hidden;
max-width: 400px;
text-overflow: ellipsis;
display: inline-block;
}
.no-ellipsize,
[no-ellipsize] {
text-overflow: '';
white-space: normal;
overflow: auto;
}
.alignSpan {
overflow: hidden;
display: inline-block;
white-space: nowrap;
& > .no-changes {
font-style: italic;
}
.json-field {
text-transform: capitalize;
}
}
}
}

View File

@ -0,0 +1,40 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('editors', {
description: 'Get the list of entity editors',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The model id',
required: true
}, {
arg: 'filter',
type: 'Object',
description: 'The user filter object'
}
],
returns: {
type: [Self],
root: true
},
http: {
path: `/:id/editors`,
verb: 'GET'
}
});
Self.editors = async(id, filter) => {
const res = await Self.find({
fields: ['userFk'],
where: {originFk: id}
});
const userIds = new Set(res.map(x => x.userFk));
filter = mergeFilters(filter, {
where: {id: {inq: [...userIds]}}
});
return await Self.app.models.VnUser.find(filter);
};
};

View File

@ -0,0 +1,44 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('models', {
description: 'Get the list of entity models',
accepts: [
{
arg: 'id',
type: 'integer',
description: 'The model id',
required: true
}, {
arg: 'filter',
type: 'Object',
description: 'The filter object'
}
],
returns: {
type: [Self],
root: true
},
http: {
path: `/:id/models`,
verb: 'GET'
}
});
Self.models = async(id, filter) => {
filter = mergeFilters(filter, {
fields: ['changedModel'],
where: {
originFk: id,
changedModel: {neq: null}
}
});
const res = await Self.find(filter);
const set = new Set();
return res.filter(x => set.has(x.changedModel)
? false
: set.add(x.changedModel)
);
};
};

View File

@ -0,0 +1,10 @@
module.exports = function(Self) {
Object.assign(Self, {
setup() {
Self.super_.setup.call(this);
require('../methods/log/editors')(this);
require('../methods/log/models')(this);
}
});
};

View File

@ -0,0 +1,4 @@
{
"name": "Log",
"base": "VnModel"
}

View File

@ -21,19 +21,21 @@ module.exports = function(Self) {
let orgBeginTransaction = this.beginTransaction;
this.beginTransaction = function(options, cb) {
options = options || {};
if (!options.timeout)
options.timeout = 120000;
if (options.timeout === undefined)
options.timeout = 120 * 1000;
return orgBeginTransaction.call(this, options, cb);
};
});
// Register field ACL validation
/* this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx));
/*
this.beforeRemote('prototype.patchAttributes', ctx => this.checkUpdateAcls(ctx));
this.beforeRemote('updateAll', ctx => this.checkUpdateAcls(ctx));
this.beforeRemote('patchOrCreate', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('create', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('replaceById', ctx => this.checkInsertAcls(ctx));
this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx)); */
this.beforeRemote('replaceOrCreate', ctx => this.checkInsertAcls(ctx));
*/
this.remoteMethod('crud', {
description: `Create, update or/and delete instances from model with a single request`,

View File

@ -156,6 +156,19 @@
"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",
"Description cannot be blank": "Description cannot be blank",
"company": "Company",
"country": "Country",
"clientId": "Id client",
"clientSocialName": "Client",
"amount": "Amount",
"taxableBase": "Taxable base",
"ticketFk": "Id ticket",
"isActive": "Active",
"hasToInvoice": "Invoice",
"isTaxDataChecked": "Data checked",
"comercialId": "Id Comercial",
"comercialName": "Comercial",
"Added observation": "Added observation",
"Comment added to client": "Comment added to client"
"Comment added to client": "Comment added to client",
"This ticket is already a refund": "This ticket is already a refund"
}

View File

@ -272,8 +272,23 @@
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Not exist this branch": "La rama no existe",
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
"Collection does not exist": "La colección no existe",
"Cannot obtain exclusive lock": "No se puede obtener un bloqueo exclusivo",
"Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}",
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen"
"Cannot create a new claimBeginning from a different ticket": "No se puede crear una línea de reclamación de un ticket diferente al origen",
"company": "Compañía",
"country": "País",
"clientId": "Id cliente",
"clientSocialName": "Cliente",
"amount": "Importe",
"taxableBase": "Base",
"ticketFk": "Id ticket",
"isActive": "Activo",
"hasToInvoice": "Facturar",
"isTaxDataChecked": "Datos comprobados",
"comercialId": "Id comercial",
"comercialName": "Comercial",
"Invalid NIF for VIES": "Invalid NIF for VIES"
}

View File

@ -15,7 +15,7 @@
"legacyUtcDateProcessing": false,
"timezone": "local",
"connectTimeout": 40000,
"acquireTimeout": 60000,
"acquireTimeout": 90000,
"waitForConnections": true
},
"osticket": {

View File

@ -0,0 +1,7 @@
name: mail
columns:
id: id
receiver: receiver
replyTo: reply to
subject: subject
body: body

View File

@ -0,0 +1,7 @@
name: mail
columns:
id: id
receiver: receptor
replyTo: responder a
subject: asunto
body: cuerpo

View File

@ -1,6 +1,6 @@
{
"name": "RoleLog",
"base": "VnModel",
"base": "Log",
"options": {
"mysql": {
"table": "account.roleLog"

View File

@ -1,6 +1,6 @@
{
"name": "UserLog",
"base": "VnModel",
"base": "Log",
"options": {
"mysql": {
"table": "account.userLog"

View File

@ -6,6 +6,7 @@
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-user-search-panel"
info="Search user by id, name or nickname"
model="model"

View File

@ -0,0 +1,6 @@
name: claim beginning
columns:
id: id
quantity: quantity
claimFk: claim
saleFk: sale

View File

@ -0,0 +1,6 @@
name: comienzo reclamación
columns:
id: id
quantity: cantidad
claimFk: reclamación
saleFk: línea

View File

@ -0,0 +1,9 @@
name: claim development
columns:
id: id
claimFk: claim
claimResponsibleFk: responsible
claimReasonFk: reason
claimResultFk: result
claimRedeliveryFk: redelivery
workerFk: worker

View File

@ -0,0 +1,9 @@
name: desarrollo reclamación
columns:
id: id
claimFk: reclamación
claimResponsibleFk: responsable
claimReasonFk: motivo
claimResultFk: resultado
claimRedeliveryFk: reenvío
workerFk: trabajador

View File

@ -0,0 +1,4 @@
name: claim dms
columns:
dmsFk: dms
claimFk: claim

View File

@ -0,0 +1,4 @@
name: documento reclamación
columns:
dmsFk: dms
claimFk: reclamación

View File

@ -0,0 +1,7 @@
name: claim end
columns:
id: id
claimFk: claim
saleFk: sale
workerFk: worker
claimDestinationFk: destination

View File

@ -0,0 +1,7 @@
name: final reclamación
columns:
id: id
claimFk: reclamación
saleFk: línea
workerFk: trabajador
claimDestinationFk: destino

View File

@ -0,0 +1,7 @@
name: claim observation
columns:
id: id
claimFk: claim
text: text
created: created
workerFk: worker

View File

@ -0,0 +1,7 @@
name: observación reclamación
columns:
id: id
claimFk: reclamación
text: texto
created: creado
workerFk: tabajador

View File

@ -0,0 +1,16 @@
name: claim
columns:
id: id
observation: observation
ticketCreated: ticket created
isChargedToMana: charged to mana
created: created
responsibility: responsibility
hasToPickUp: has to pickUp
ticketFk: ticket
claimStateFk: claim state
workerFk: worker
packages: packages
rma: rma
clientFk: client
claimFk: claim

View File

@ -0,0 +1,16 @@
name: reclamación
columns:
id: id
observation: observación
ticketCreated: ticket creado
isChargedToMana: cargado al maná
created: creado
responsibility: responsabilidad
hasToPickUp: es recogida
ticketFk: ticket
claimStateFk: estado reclamación
workerFk: trabajador
packages: paquetes
rma: rma
clientFk: cliente
claimFk: reclamación

View File

@ -109,6 +109,11 @@ module.exports = Self => {
zoneFk: zone.id
}, myOptions);
await models.TicketRefund.create({
refundTicketFk: newRefundTicket.id,
originalTicketFk: claim.ticket().id
}, myOptions);
await saveObservation({
description: `Reclama ticket: ${claim.ticketFk}`,
ticketFk: newRefundTicket.id,

View File

@ -1,6 +1,6 @@
{
"name": "ClaimLog",
"base": "VnModel",
"base": "Log",
"options": {
"mysql": {
"table": "claimLog"

View File

@ -16,7 +16,7 @@
value="{{$ctrl.claimedTotal | currency: 'EUR':2}}">
</vn-label-value>
</vn-card>
<vn-card class="vn-pa-lg vn-w-lg">
<vn-card class="vn-pa-md vn-w-lg">
<smart-table
model="model"
options="$ctrl.smartTableOptions"
@ -45,13 +45,13 @@
step="1"
on-change="$ctrl.save({responsibility: value})">
</vn-range>
</vn-tool-bar>
<vn-check class="right"
vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"
on-change="$ctrl.save({isChargedToMana: value})">
</vn-check>
</vn-tool-bar>
</section>
</slot-actions>
<slot-table>

View File

@ -17,6 +17,10 @@ class Controller extends Descriptor {
}
sendPickupOrder() {
if (!this.claim.client.email) {
this.vnApp.showError(this.$t('The client does not have an email'));
return;
}
return this.vnEmail.send(`Claims/${this.claim.id}/claim-pickup-email`, {
recipient: this.claim.client.email,
recipientId: this.claim.clientFk

View File

@ -20,3 +20,4 @@ Photos: Fotos
Go to the claim: Ir a la reclamación
Sale tracking: Líneas preparadas
Ticket tracking: Estados del ticket
The client does not have an email: El cliente no tiene email

View File

@ -1,48 +1 @@
<vn-crud-model
vn-id="model"
auto-load="true"
filter="::$ctrl.filter"
url="ClaimDms"
link="{claimFk: $ctrl.$params.id}"
limit="20"
data="$ctrl.photos">
</vn-crud-model>
<vn-horizontal class="photo-list drop-zone" vn-droppable="$ctrl.onDrop($event)">
<section class="empty-rows" ng-if="!$ctrl.photos.length">
<section><vn-icon icon="image"></vn-icon></section>
<section translate>Drag & Drop photos here...</section>
</section>
<section class="photo" ng-repeat="photo in $ctrl.photos">
<section class="image vn-shadow" on-error-src
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"
ng-if="photo.dms.contentType != 'video/mp4'">
</section>
<video id="videobcg" muted="muted" controls ng-if="photo.dms.contentType == 'video/mp4'"
class="video">
<source src="{{$ctrl.getImagePath(photo.dmsFk)}}" type="video/mp4">
</video>
<section class="actions">
<vn-button
class="round"
ng-click="confirm.show($index)"
title="{{'Remove file' | translate}}"
tabindex="-1"
icon="delete">
</vn-button>
</section>
</section>
</vn-horizontal>
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure you want to continue?"
on-accept="$ctrl.deleteDms($data)">
</vn-confirm>
<vn-float-button
icon="add"
vn-tooltip="Select file"
vn-bind="+"
ng-click="$ctrl.openUploadDialog()"
fixed-bottom-right>
</vn-float-button>

View File

@ -1,105 +1,17 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $, vnFile) {
constructor($element, $) {
super($element, $);
this.vnFile = vnFile;
this.filter = {
include: [
{
relation: 'dms'
}
]
};
}
deleteDms(index) {
const dmsFk = this.photos[index].dmsFk;
return this.$http.post(`ClaimDms/${dmsFk}/removeFile`)
.then(() => {
this.$.model.remove(index);
this.vnApp.showSuccess(this.$t('File deleted'));
});
}
onDrop($event) {
const files = $event.dataTransfer.files;
this.setDefaultParams().then(() => {
this.dms.files = files;
this.create();
});
}
setDefaultParams() {
const filter = {
where: {code: 'claim'}
};
return this.$http.get('DmsTypes/findOne', {filter}).then(res => {
const dmsTypeId = res.data && res.data.id;
const companyId = this.vnConfig.companyFk;
const warehouseId = this.vnConfig.warehouseFk;
this.dms = {
hasFile: false,
hasFileAttached: false,
reference: this.claim.id,
warehouseId: warehouseId,
companyId: companyId,
dmsTypeId: dmsTypeId,
description: this.$t('FileDescription', {
claimId: this.claim.id,
clientId: this.claim.client.id,
clientName: this.claim.client.name
}).toUpperCase()
};
});
}
openUploadDialog() {
const element = document.createElement('input');
element.setAttribute('type', 'file');
element.setAttribute('multiple', true);
element.click();
element.addEventListener('change', () =>
this.setDefaultParams().then(() => {
this.dms.files = element.files;
this.create();
})
);
}
create() {
const query = `claims/${this.claim.id}/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {'Content-Type': undefined},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(() => {
this.vnApp.showSuccess(this.$t('File uploaded!'));
this.$.model.refresh();
});
}
getImagePath(dmsId) {
return this.vnFile.getPath(`/api/Claims/${dmsId}/downloadFile`);
async $onInit() {
const url = await this.vnApp.getUrl(`claim/${this.$params.id}/photos`);
window.location.href = url;
}
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.vnComponent('vnClaimPhotos', {
template: require('./index.html'),
controller: Controller,

View File

@ -1,70 +0,0 @@
import './index';
import crudModel from 'core/mocks/crud-model';
describe('Claim', () => {
describe('Component vnClaimPhotos', () => {
let $scope;
let $httpBackend;
let controller;
beforeEach(ngModule('claim'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
controller = $componentController('vnClaimPhotos', {$element: null, $scope});
controller.$.model = crudModel;
controller.claim = {
id: 1,
client: {id: 1101, name: 'Bruce Wayne'}
};
}));
describe('deleteDms()', () => {
it('should make an HTTP Post query', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'remove');
const dmsId = 1;
const dmsIndex = 0;
controller.photos = [{dmsFk: 1}];
$httpBackend.expectPOST(`ClaimDms/${dmsId}/removeFile`).respond();
controller.deleteDms(dmsIndex);
$httpBackend.flush();
expect(controller.$.model.remove).toHaveBeenCalledWith(dmsIndex);
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('setDefaultParams()', () => {
it('should make an HTTP GET query, then set all dms properties', () => {
$httpBackend.expectRoute('GET', `DmsTypes/findOne`).respond({});
controller.setDefaultParams();
$httpBackend.flush();
expect(controller.dms).toBeDefined();
});
});
describe('create()', () => {
it('should make an HTTP Post query, then refresh the model data', () => {
const claimId = 1;
const dmsIndex = 0;
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller.$.model, 'refresh');
controller.photos = [{dmsFk: 1}];
controller.dmsIndex = dmsIndex;
controller.dms = {files: []};
$httpBackend.expectPOST(`claims/${claimId}/uploadFile`).respond({});
controller.create();
$httpBackend.flush();
expect(controller.$.model.refresh).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
});
});

View File

@ -1,5 +0,0 @@
Are you sure you want to continue?: ¿Seguro que quieres continuar?
Drag & Drop photos here...: Arrastra y suelta fotos aquí...
File deleted: Archivo eliminado
File uploaded!: Archivo subido!
Select file: Seleccionar fichero

View File

@ -1,47 +0,0 @@
@import "./variables";
vn-claim-photos {
height: 100%;
.drop-zone {
color: $color-font-secondary;
box-sizing: border-box;
border-radius: 8px;
text-align: center;
min-height: 100%;
.empty-rows {
padding: 80px $spacing-md;
font-size: 1.375rem
}
vn-icon {
font-size: 3rem
}
}
.photo-list {
padding: $spacing-md;
min-height: 100%;
.photo {
width: 512px;
height: 288px;
}
}
.video {
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),
0 3px 1px -2px rgba(0,0,0,.2),
0 1px 5px 0 rgba(0,0,0,.12);
border: 2px solid transparent;
}
.video:hover {
border: 2px solid $color-primary
}
}

View File

@ -0,0 +1,20 @@
name: address
columns:
id: id
nickname: nickname
street: street
city: city
postalCode: postal code
phone: phone
mobile: mobile
isActive: active
longitude: longitude
latitude: latitude
isEqualizated: equalizated
isLogifloraAllowed: logiflora allowed
provinceFk: province
clientFk: client
agencyModeFk: agency
addressFk: address
incotermsFk: incoterms
customsAgentFk: customs agent

View File

@ -0,0 +1,20 @@
name: dirección
columns:
id: id
nickname: apodo
street: calle
city: ciudad
postalCode: código postal
phone: teléfono
mobile: móvil
isActive: activo
longitude: longitud
latitude: latitud
isEqualizated: igualado
isLogifloraAllowed: logiflora permitido
provinceFk: provincia
clientFk: cliente
agencyModeFk: agencia
addressFk: dirección
incotermsFk: incoterms
customsAgentFk: agente adunanas

View File

@ -0,0 +1,6 @@
name: client contact
columns:
id: id
name: name
phone: phone
clientFk: client

View File

@ -0,0 +1,6 @@
name: contacto cliente
columns:
id: id
name: nombre
phone: teléfono
clientFk: cliente

View File

@ -0,0 +1,4 @@
name: client dms
columns:
dmsFk: dms
clientFk: client

View File

@ -0,0 +1,4 @@
name: documento cliente
columns:
dmsFk: dms
clientFk: client

View File

@ -0,0 +1,7 @@
name: client observation
columns:
id: id
clientFk: client
text: text
created: created
workerFk: worker

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