Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4764-serviceAbono

This commit is contained in:
Carlos Satorres 2023-04-24 09:04:33 +02:00
commit 5ec0cd962d
53 changed files with 4921 additions and 1246 deletions

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

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

@ -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),
@ -1200,6 +1196,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),

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

View File

@ -66,7 +66,6 @@
</vn-tr>
</vn-tbody>
</vn-table>
<vn-pagination model="model"></vn-pagination>
</vn-card>
</vn-data-viewer>
<vn-worker-descriptor-popover vn-id="workerDescriptor">

View File

@ -21,8 +21,8 @@ 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);
};
});

View File

@ -272,6 +272,8 @@
"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}}",

View File

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

View File

@ -21,7 +21,7 @@ module.exports = Self => {
id,
params
FROM clientConsumptionQueue
WHERE status = ''`);
WHERE status = '' OR status IS NULL`);
for (const queue of queues) {
try {
@ -44,6 +44,7 @@ module.exports = Self => {
GROUP BY c.id`, [params.clients, params.from, params.to]);
for (const client of clients) {
try {
const args = {
id: client.clientFk,
recipient: client.clientEmail,
@ -54,6 +55,12 @@ module.exports = Self => {
const email = new Email('campaign-metrics', args);
await email.send();
} catch (error) {
if (error.code === 'EENVELOPE')
continue;
throw error;
}
}
await Self.rawSql(`

View File

@ -0,0 +1,55 @@
module.exports = Self => {
Self.remoteMethodCtx('setRating', {
description: 'Change rating and recommendedCredit of a client',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'rating',
type: 'number'
},
{
arg: 'recommendedCredit',
type: 'number'
}
],
http: {
path: `/:id/setRating`,
verb: 'POST'
}
});
Self.setRating = async function(ctx, id, rating, recommendedCredit, options) {
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const client = await Self.findById(id, null, myOptions);
const clientUpdated = await client.updateAttributes({
rating: rating,
recommendedCredit: recommendedCredit
}, myOptions);
if (tx) await tx.commit();
return clientUpdated;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,43 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Client setRating()', () => {
const financialId = 73;
const activeCtx = {
accessToken: {userId: financialId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should change rating and recommendedCredit', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const clientId = 1101;
const newRating = 10;
const newRecommendedCredit = 20;
const updatedClient = await models.Client.setRating(ctx, clientId, newRating, newRecommendedCredit, options);
expect(updatedClient.rating).toEqual(newRating);
expect(updatedClient.recommendedCredit).toEqual(newRecommendedCredit);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -32,6 +32,9 @@
"ClientConsumptionQueue": {
"dataSource": "vn"
},
"ClientInforma": {
"dataSource": "vn"
},
"ClientLog": {
"dataSource": "vn"
},

View File

@ -0,0 +1,42 @@
{
"name": "ClientInforma",
"base": "Loggable",
"log": {
"model":"ClientLog",
"relation": "client",
"showField": "clientFk"
},
"options": {
"mysql": {
"table": "clientInforma"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"rating": {
"type": "number"
},
"recommendedCredit": {
"type": "number"
},
"created": {
"type": "date"
}
},
"relations": {
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"client": {
"type": "belongsTo",
"model": "Client",
"foreignKey": "clientFk"
}
}
}

View File

@ -47,4 +47,5 @@ module.exports = Self => {
require('../methods/client/consumptionSendQueued')(Self);
require('../methods/client/filter')(Self);
require('../methods/client/getClientOrSupplierReference')(Self);
require('../methods/client/setRating')(Self);
};

View File

@ -280,6 +280,10 @@ module.exports = Self => {
if (changes.credit !== undefined)
await Self.changeCredit(ctx, finalState, changes);
// Credit management changes
if (orgData?.rating != changes.rating || orgData?.recommendedCredit != changes.recommendedCredit)
await Self.changeCreditManagement(ctx, finalState, changes);
const oldInstance = {};
if (!ctx.isNewInstance) {
const newProps = Object.keys(changes);
@ -441,6 +445,19 @@ module.exports = Self => {
}, ctx.options);
};
Self.changeCreditManagement = async function changeCreditManagement(ctx, finalState, changes) {
const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext();
const userId = loopBackContext.active.accessToken.userId;
await models.ClientInforma.create({
clientFk: finalState.id,
rating: changes.rating,
recommendedCredit: changes.recommendedCredit,
workerFk: userId
}, ctx.options);
};
const app = require('vn-loopback/server/server');
app.on('started', function() {
const VnUser = app.models.VnUser;

View File

@ -141,6 +141,12 @@
},
"hasElectronicInvoice": {
"type": "boolean"
},
"rating": {
"type": "number"
},
"recommendedCredit": {
"type": "number"
}
},

View File

@ -0,0 +1,79 @@
<mg-ajax path="Clients/{{post.params.id}}/setRating" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
url="Clients"
data="$ctrl.client"
id-value="$ctrl.$params.id"
insert-mode="true"
form="form"
save="post">
</vn-watcher>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-input-number
vn-one
label="Rating"
ng-model="$ctrl.client.rating"
vn-focus
rule>
</vn-input-number>
<vn-input-number
vn-one
label="Recommended credit"
ng-model="$ctrl.client.recommendedCredit"
rule>
</vn-input-number>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit>
</vn-button-bar>
</form>
<vn-crud-model
vn-id="model"
url="ClientInformas"
filter="$ctrl.filter"
link="{clientFk: $ctrl.$params.id}"
limit="20"
data="clientInformas"
order="created DESC"
auto-load="true">
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-md">
<vn-card>
<vn-table model="model" class="vn-mt-lg">
<vn-thead>
<vn-tr>
<vn-th shrink-date field="created">Since</vn-th>
<vn-th field="workerFk">Employee</vn-th>
<vn-th field="rating" number>Rating</vn-th>
<vn-th field="recommendedCredit" number>Recommended credit</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="clientInforma in clientInformas">
<vn-td shrink-datetime>{{::clientInforma.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td shrink>
<span
ng-click="workerDescriptor.show($event, clientInforma.workerFk)"
class="link">
{{::clientInforma.worker.user.nickname}}
</span>
</vn-td>
<vn-td number>{{::clientInforma.rating}}</vn-td>
<vn-td number>{{::clientInforma.recommendedCredit}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,32 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
include: [{
relation: 'worker',
scope: {
fields: ['userFk'],
include: {
relation: 'user',
scope: {
fields: ['nickname']
}
}
}
}],
};
}
onSubmit() {
this.$.watcher.submit()
.then(() => this.$state.reload());
}
}
ngModule.vnComponent('vnClientCreditManagement', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,2 @@
Recommended credit: Crédito recomendado
Rating: Clasificación

View File

@ -47,3 +47,5 @@ import './defaulter';
import './notification';
import './unpaid';
import './extended-list';
import './credit-management';

View File

@ -64,4 +64,6 @@ Compensation Account: Cuenta para compensar
Amount to return: Cantidad a devolver
Delivered amount: Cantidad entregada
Unpaid: Impagado
Credit management: Gestión de crédito
Credit opinion: Opinión de crédito
There is no zona: No hay zona

View File

@ -23,6 +23,14 @@
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"},
{
"description": "Credit management",
"icon": "monetization_on",
"childs": [
{"state": "client.card.creditInsurance.index", "icon": "icon-solunion"},
{"state": "client.card.creditManagement", "icon": "contact_support"}
]
},
{
"description": "Others",
"icon": "more",
@ -30,7 +38,6 @@
{"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.consumption", "icon": "show_chart"},
{"state": "client.card.mandate", "icon": "pan_tool"},
{"state": "client.card.creditInsurance.index", "icon": "icon-solunion"},
{"state": "client.card.contact", "icon": "contact_phone"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms.index", "icon": "cloud_upload"},
@ -416,7 +423,8 @@
"state": "client.notification",
"component": "vn-client-notification",
"description": "Notifications"
}, {
},
{
"url": "/unpaid",
"state": "client.card.unpaid",
"component": "vn-client-unpaid",
@ -428,6 +436,13 @@
"state": "client.extendedList",
"component": "vn-client-extended-list",
"description": "Extended list"
},
{
"url": "/credit-management",
"state": "client.card.creditManagement",
"component": "vn-client-credit-management",
"acl": ["financial"],
"description": "Credit opinion"
}
]
}

View File

@ -1,3 +1,6 @@
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<vn-crud-model
vn-id="ticketsModel"
url="Tickets"
@ -253,7 +256,11 @@
</vn-label-value>
</vn-one>
<vn-one>
<h4 translate>Financial information</h4>
<h4 ng-show="$ctrl.isEmployee">
<a href="https://grafana.verdnatura.es/d/40buzE4Vk/comportamiento-pagos-clientes?orgId=1&var-clientFk={{::$ctrl.client.id}}">
<span class="grafana" translate vn-tooltip="Go to grafana">Billing data</span>
</a>
</h4>
<vn-label-value label="Risk"
value="{{$ctrl.summary.debt.debt | currency: 'EUR':2}}"
ng-class="{alert: $ctrl.summary.debt.debt > $ctrl.summary.credit}"
@ -282,6 +289,10 @@
ng-if="$ctrl.summary.recovery.started"
value="{{$ctrl.summary.recovery.started | date:'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Rating"
value="{{$ctrl.summary.rating}}"
info="Value from 1 to 20. The higher the better value">
</vn-label-value>
</vn-one>
</vn-horizontal>
<vn-horizontal>

View File

@ -20,3 +20,6 @@ Invoices minus payments: Facturas menos recibos
Deviated invoices minus payments: Facturas fuera de plazo menos recibos
Go to the client: Ir al cliente
Latest tickets: Últimos tickets
Rating: Clasificación
Value from 1 to 20. The higher the better value: Valor del 1 al 20. Cuanto más alto mejor valoración
Go to grafana: Ir a grafana

View File

@ -6,4 +6,9 @@ vn-client-summary .summary {
.alert span {
color: $color-alert !important
}
vn-horizontal h4 .grafana:after {
content: 'contact_support';
font-size: 17px;
}
}

View File

@ -98,6 +98,7 @@ module.exports = Self => {
stmt.merge(conn.makeWhere(args.filter.where));
stmt.merge(conn.makeOrderBy(args.filter.order));
stmt.merge(conn.makeLimit(args.filter));
const negativeBasesIndex = stmts.push(stmt) - 1;

View File

@ -2,7 +2,8 @@
vn-id="model"
url="InvoiceIns/negativeBases"
auto-load="true"
params="$ctrl.params">
params="$ctrl.params"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
</vn-portal>

View File

@ -37,7 +37,7 @@ describe('Item editFixedPrice()', () => {
const options = {transaction: tx};
try {
const filter = {'it.categoryFk': 1};
const filter = {where: {'it.categoryFk': 1}};
const ctx = {
args: {
filter: filter
@ -48,7 +48,7 @@ describe('Item editFixedPrice()', () => {
const field = 'rate2';
const newValue = 88;
await models.FixedPrice.editFixedPrice(ctx, field, newValue, null, filter, options);
await models.FixedPrice.editFixedPrice(ctx, field, newValue, null, filter.where, options);
const [result] = await models.FixedPrice.filter(ctx, filter, options);

View File

@ -68,7 +68,9 @@
<th field="stateFk">
<span translate>State</span>
</th>
<th field="isFragile"></th>
<th field="isFragile" number>
<span translate>Fragile</span>
</th>
<th field="zoneFk">
<span translate>Zone</span>
</th>

View File

@ -30,35 +30,47 @@ module.exports = Self => {
const ticketLogs = await models.TicketLog.find(
{
where: {
or: [
{
and: [
{originFk: id},
{action: 'update'},
{changedModel: 'Sale'}
]
},
{
and: [
{originFk: id},
{action: 'delete'},
{changedModel: 'Sale'}
]
}
]
},
fields: [
'oldInstance',
'newInstance',
'changedModelId'
'changedModelId',
'changedModelValue'
],
}, myOptions);
const changes = [];
for (const ticketLog of ticketLogs) {
const oldQuantity = ticketLog.oldInstance.quantity;
const newQuantity = ticketLog.newInstance.quantity;
for (const log of ticketLogs) {
const oldQuantity = log.oldInstance.quantity;
const newQuantity = log.newInstance?.quantity || 0;
if (oldQuantity || newQuantity) {
const sale = await models.Sale.findById(ticketLog.changedModelId, null, myOptions);
const message = $t('Change quantity', {
concept: sale.concept,
const changeMessage = $t('Change quantity', {
concept: log.changedModelValue,
oldQuantity: oldQuantity || 0,
newQuantity: newQuantity || 0,
});
changes.push(changeMessage);
}
}
changes.push(message);
}
}
return changes.join('\n');
};
};

View File

@ -11,8 +11,7 @@ module.exports = Self => {
http: {source: 'path'}
}, {
arg: 'userFk',
type: 'number',
required: true,
type: 'any',
description: 'The user id'
}
],

View File

@ -100,7 +100,7 @@ module.exports = Self => {
dmsTypeId: dmsType.id,
reference: '',
description: `Firma del cliente - Ruta ${ticket.route().id}`,
hasFile: true
hasFile: false
};
dms = await models.Dms.uploadFile(ctxUploadFile, myOptions);
gestDocCreated = true;

View File

@ -23,6 +23,9 @@
"DeviceProduction": {
"dataSource": "vn"
},
"DeviceProductionLog": {
"dataSource": "vn"
},
"DeviceProductionModels": {
"dataSource": "vn"
},

View File

@ -0,0 +1,55 @@
{
"name": "DeviceProductionLog",
"base": "VnModel",
"options": {
"mysql": {
"table": "deviceProductionLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"deviceProduction": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"created": {
"type": "date"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"changedModel": {
"type": "string"
},
"changedModelId": {
"type": "number"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["created DESC", "id DESC"]
}
}

View File

@ -1,6 +1,10 @@
{
"name": "DeviceProductionUser",
"base": "VnModel",
"base": "Loggable",
"log": {
"model": "DeviceProductionLog",
"relation": "deviceProduction"
},
"options": {
"mysql": {
"table": "deviceProductionUser"

View File

@ -1,6 +1,9 @@
{
"name": "DeviceProduction",
"base": "VnModel",
"base": "Loggable",
"log": {
"model": "DeviceProductionLog"
},
"options": {
"mysql": {
"table": "deviceProduction"

View File

@ -31,11 +31,12 @@
value-field="id"
show-field="serialNumber">
<tpl-item>
<span>ID: {{id}}</span>
<span class="separator"></span>
<span>{{'Model' | translate}}: {{modelFk}}</span>
<span class="separator"></span>
<span>{{'Serial Number' | translate}}: {{serialNumber}}</span>
<div>
ID: {{id}}
</div>
<div class="text-caption text-grey">
{{modelFk}}, {{serialNumber}}
</div>
</tpl-item>
</vn-autocomplete>
</vn-horizontal>

View File

@ -1,6 +1,6 @@
span.separator{
border-left: 1px solid black;
height: 100%;
margin: 0 10px;
@import "./variables";
.text-grey {
color: $color-font-light;
}

4957
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,9 +24,11 @@ sections:
dicho stock puede variar en función de la fecha seleccionada al configurar el
pedido. Es importante CONFIRMAR los pedidos para que la mercancía quede reservada.
delivery: El reparto se realiza de lunes a sábado según la zona en la que te encuentres.
Por regla general, los pedidos que se entregan por agencia, deben estar confirmados
y pagados antes de las 17h del día en que se preparan (el día anterior a recibirlos),
aunque esto puede variar si el pedido se envía a través de nuestro reparto y
Los pedidos que se entregan por agencia o por reparto Verdnatura deben estar confirmados y pagados
antes del cierre de la correspondiente ruta el mismo día de preparación del pedido. Este horario
puede variar mucho en función de la ruta y del volumen de pedidos para ese día, por lo que es
recomendable no apurar el tiempo y dejar todo listo a primera hora del día de preparación del pedido.
Aunque esto puede variar si el pedido se envía a través de nuestro reparto y
según la zona.
howToPay:
title: Cómo pagar

View File

@ -0,0 +1,51 @@
subject: Bienvenue chez Verdnatura
title: "Nous vous souhaitons la bienvenue!"
dearClient: Cher client
clientData: 'Vos données pour pouvoir acheter sur le site de Verdnatura (<a href="https://shop.verdnatura.es"
title="Visiter Verdnatura" target="_blank" style="color: #8dba25">https://shop.verdnatura.es</a>)
ou sur nos applications pour <a href="https://goo.gl/3hC2mG" title="App Store"
target="_blank" style="color: #8dba25">iOS</a> et <a href="https://goo.gl/8obvLc"
title="Google Play" target="_blank" style="color: #8dba25">Android</a>, sont'
clientId: Identifiant du client
user: Utilisateur
password: Mot de passe
passwordResetText: Cliquez sur "Vous avez oublié votre mot de passe?"
sections:
howToBuy:
title: Comment passer une commande
description: 'Pour passer une commande sur notre site, vous devez configurer celle-ci en indiquant :'
requeriments:
- Si vous souhaitez recevoir la commande (par agence ou par notre propre livraison)
ou si vous préférez la récupérer dans l'un de nos entrepôts.
- La date à laquelle vous souhaitez recevoir la commande (elle sera préparée la veille).
- L'adresse de livraison ou l'entrepôt où vous souhaitez récupérer la commande.
stock: Sur notre site et nos applications, vous pouvez visualiser le stock disponible de
fleurs coupées, feuillages, plantes, accessoires et artificiels. Veuillez noter que ce
stock peut varier en fonction de la date sélectionnée lors de la configuration de la
commande. Il est important de CONFIRMER les commandes pour que la marchandise soit réservée.
delivery: La livraison est effectuée du lundi au samedi selon la zone dans laquelle vous
vous trouvez. Les commandes livrées par agence ou par notre propre livraison doivent
être confirmées et payées avant la fermeture de la route correspondante le jour même
de la préparation de la commande. Cet horaire peut varier considérablement en fonction
de la route et du volume de commandes pour cette journée, il est donc recommandé de ne
pas attendre la dernière minute et de tout préparer tôt le matin le jour de la
préparation de la commande. Cela peut toutefois varier si la commande est envoyée par
notre propre livraison et en fonction de la zone.
howToPay:
title: Comment payer
description: 'Les moyens de paiement acceptés chez Verdnatura sont les suivants :'
options:
- Par <strong>carte de crédit</strong> via notre plateforme web (lors de la confirmation de la commande).
- Par <strong>virement bancaire mensuel</strong>, modalité à demander et à gérer.
toConsider:
title: Choses à prendre en compte
description: Verdnatura vend EXCLUSIVEMENT aux professionnels, vous devez donc nous envoyer
le modèle 036 ou 037 pour vérifier que vous êtes bien inscrit dans la catégorie du commerce de fleurs.
claimsPolicy:
title: POLITIQUE DE RÉCLAMATION
description: Verdnatura acceptera les réclamations effectuées dans les deux jours civils suivant
la réception de la commande (y compris le jour même de la réception). Passé ce délai, aucune réclamation ne sera acceptée.
help: Si vous avez des questions, n'hésitez pas à nous contacter, <strong>nous sommes là pour vous aider !</strong>
salesPersonName: Je suis votre commercial et mon nom est
salesPersonPhone: Téléphone et Whatsapp
salesPersonEmail: Adresse e-mail

View File

@ -0,0 +1,50 @@
subject: Bem-Vindo à Verdnatura
title: "Damos-te as boas-vindas!"
dearClient: Estimado cliente
clientData: 'seus dados para poder comprar na loja online da Verdnatura (<a href="https://shop.verdnatura.es"
title="Visitar Verdnatura" target="_blank" style="color: #8dba25">https://shop.verdnatura.es</a>)
ou em nossos aplicativos para <a href="https://goo.gl/3hC2mG" title="App Store" target="_blank" style="color: #8dba25">iOS</a>
e <a href="https://goo.gl/8obvLc" title="Google Play" target="_blank" style="color: #8dba25">Android</a>, são'
clientId: Identificador de cliente
user: Utilizador
password: Palavra-passe
passwordResetText: Clique em 'Esqueceu a sua palavra-passe?'
sections:
howToBuy:
title: Como fazer uma encomenda
description: 'Para realizar uma encomenda no nosso site, deves configurá-la indicando:'
requeriments:
- Se queres receber a encomenda (por agência ou o nosso próprio transporte) ou se preferes levantá-lo em algum dos nossos armazéns.
- A data que queres receber a encomenda (se preparará no dia anterior).
- A morada de entrega ou armazém aonde queres levantar a encomenda.
stock: No nosso site e apps podes visualizar o estoque disponível de
flor-de-corte, verduras, plantas, acessórios e artificial. Tenha presente que
dito estoque pode variar em função da data escolhida ao configurar a
encomenda. É importante confirmar as encomendas para que a mercadoria fique reservada.
delivery: O transporte se realiza de terça a sabado. As encomendas que se entreguem por agências ou transporte Verdnatura, devem estar confirmadas e pagas até
antes do horário de encerre da correspondente rota do dia de preparação da mesma. Este horario
pode variar muito em função da rota e o volume de encomendas deste dia, pelo qual é
recomendável não esperar à última hora e deixar tudo pronto à primeira hora do dia de preparação. Ainda que isto possa variar se a encomenda se envia através do nosso transporte
dependendo da zona.
howToPay:
title: Como pagar
description: 'As formas de pagamentos admitidas na Verdnatura são:'
options:
- Com <strong>cartão</strong> através da plataforma de pagamentos (ao confirmar a encomenda ou entrando em Encomendas > Confirmadas).
- Mediante <strong>débito automatico mensual</strong>, modalidade que deve-se solicitar e tramitar.
toConsider:
title: Coisas a ter em conta
description: A Verdnatura vende EXCLUSIVAMENTE a profissionais, pelo qual deves
remetir-nos o documento de Inicio de Actividade, para comprobar-mos que o vosso CAE
esteja relacionado com o mundo das flores.
claimsPolicy:
title: POLÍTICA DE RECLAMAÇÕES
description: A Verdnatura aceitará as reclamações que se realizem dentro dos
dois dias naturais seguintes à recepção da encomenda (incluindo o mesmo
dia da receção). Passado este prazo não se aceitará nenhuma reclamação.
help: Qualquer dúvida que lhe surja, não hesite em consultá-la <strong>estamos
para atender-te!</strong>
salesPersonName: Sou o seu asesor comercial e o meu nome é
salesPersonPhone: Telemovel e whatsapp
salesPersonEmail: Correio eletrônico

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<table class="mainTable">
<tbody>
@ -30,12 +31,16 @@
<img :src="QR" id="QR" />
<div id="right">
<div id="additionalInfo" class="ellipsize"><b>Pallet: </b>{{id}}</div>
<div id="additionalInfo" class="ellipsize"><b>User: </b> {{username.name || '---'}}</div>
<div id="additionalInfo" class="ellipsize"><b>Day: </b>{{labelData.dayName.toUpperCase() || '---'}}</div>
<div id="additionalInfo" class="ellipsize"><b>User: </b>
{{ (username ? username.name : '---')}}
</div>
<div id="additionalInfo" class="ellipsize"><b>Day: </b>{{labelData.dayName.toUpperCase() ||
'---'}}</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -14,13 +14,13 @@ module.exports = {
},
userFk: {
type: Number,
required: true,
description: 'The user id'
}
},
async serverPrefetch() {
this.username = null;
this.labelsData = await this.rawSqlFromDef('labelData', this.id);
this.username = await this.findOneFromDef('username', this.userFk);
if (this.userFk) this.username = await this.findOneFromDef('username', this.userFk);
this.labelData = this.labelsData[0];
this.checkMainEntity(this.labelData);