Merge branch 'dev' into 5075-client_balance
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Pablo Natek 2023-04-24 15:33:28 +00:00
commit 3e15728db4
57 changed files with 4934 additions and 1254 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,54 +10,68 @@ BEGIN
* de sus tickets.
*
* @param vRouteFk
*
* @select Información de los tickets
*/
SELECT
t.id Id,
t.clientFk Client,
a.id Address,
t.packages Packages,
a.street AddressName,
a.postalCode PostalCode,
a.city City,
sub2.itemPackingTypeFk PackingType,
c.phone ClientPhone,
c.mobile ClientMobile,
a.phone AddressPhone,
a.mobile AddressMobile,
d.longitude Longitude,
d.latitude Latitude,
wm.mediaValue SalePersonPhone,
tob.Note Note,
t.isSigned Signed
FROM ticket t
JOIN client c ON t.clientFk = c.id
JOIN address a ON t.addressFk = a.id
LEFT JOIN delivery d ON t.id = d.ticketFk
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
LEFT JOIN
(SELECT tob.description Note, t.id
FROM ticketObservation tob
JOIN ticket t ON tob.ticketFk = t.id
JOIN observationType ot ON ot.id = tob.observationTypeFk
WHERE t.routeFk = vRouteFk
AND ot.code = 'delivery'
)tob ON tob.id = t.id
LEFT JOIN
(SELECT sub.ticketFk,
CONCAT('(', GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk ORDER BY sub.items DESC SEPARATOR ','), ') ') itemPackingTypeFk
FROM (SELECT s.ticketFk , i.itemPackingTypeFk, COUNT(*) items
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
WHERE t.routeFk = vRouteFk
GROUP BY t.id,i.itemPackingTypeFk)sub
GROUP BY sub.ticketFk
) sub2 ON sub2.ticketFk = t.id
WHERE t.routeFk = vRouteFk
GROUP BY t.id
ORDER BY t.priority;
SELECT *
FROM (
SELECT t.id Id,
t.clientFk Client,
a.id Address,
a.nickname ClientName,
t.packages Packages,
a.street AddressName,
a.postalCode PostalCode,
a.city City,
sub2.itemPackingTypeFk PackingType,
c.phone ClientPhone,
c.mobile ClientMobile,
a.phone AddressPhone,
a.mobile AddressMobile,
d.longitude Longitude,
d.latitude Latitude,
wm.mediaValue SalePersonPhone,
tob.description Note,
t.isSigned Signed,
t.priority
FROM ticket t
JOIN client c ON t.clientFk = c.id
JOIN address a ON t.addressFk = a.id
LEFT JOIN delivery d ON d.ticketFk = t.id
LEFT JOIN workerMedia wm ON wm.workerFk = c.salesPersonFk
LEFT JOIN(
SELECT tob.description, t.id
FROM ticketObservation tob
JOIN ticket t ON tob.ticketFk = t.id
JOIN observationType ot ON ot.id = tob.observationTypeFk
WHERE t.routeFk = vRouteFk
AND ot.code = 'delivery'
)tob ON tob.id = t.id
LEFT JOIN(
SELECT sub.ticketFk,
CONCAT('(',
GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk
ORDER BY sub.items DESC SEPARATOR ','),
') ') itemPackingTypeFk
FROM (
SELECT s.ticketFk, i.itemPackingTypeFk, COUNT(*) items
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
WHERE t.routeFk = vRouteFk
GROUP BY t.id, i.itemPackingTypeFk
)sub
GROUP BY sub.ticketFk
)sub2 ON sub2.ticketFk = t.id
WHERE t.routeFk = vRouteFk
ORDER BY d.id DESC
LIMIT 10000000000000000000
)sub3
GROUP BY sub3.id
ORDER BY sub3.priority;
END$$
DELIMITER ;
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

@ -158,4 +158,4 @@
"Description cannot be blank": "Description cannot be blank",
"Added observation": "Added observation",
"Comment added to client": "Comment added to client"
}
}

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

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

@ -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,16 +44,23 @@ module.exports = Self => {
GROUP BY c.id`, [params.clients, params.from, params.to]);
for (const client of clients) {
const args = {
id: client.clientFk,
recipient: client.clientEmail,
replyTo: client.salesPersonEmail,
from: params.from,
to: params.to
};
try {
const args = {
id: client.clientFk,
recipient: client.clientEmail,
replyTo: client.salesPersonEmail,
from: params.from,
to: params.to
};
const email = new Email('campaign-metrics', args);
await email.send();
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
There is no zona: No hay zona
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"
@ -22,75 +25,75 @@
<vn-horizontal>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.basicData({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Basic data</span>
</a>
</h4>
<h4
<h4
translate
ng-show="!$ctrl.isEmployee">
Basic data
</h4>
<vn-label-value label="Id"
<vn-label-value label="Id"
value="{{$ctrl.summary.id}}">
</vn-label-value>
<vn-label-value label="Comercial Name"
<vn-label-value label="Comercial Name"
value="{{$ctrl.summary.name}}">
</vn-label-value>
<vn-label-value label="Contact"
<vn-label-value label="Contact"
value="{{$ctrl.summary.contact}}">
</vn-label-value>
<vn-label-value label="Phone"
<vn-label-value label="Phone"
value="{{$ctrl.summary.phone}}">
</vn-label-value>
<vn-label-value label="Mobile"
<vn-label-value label="Mobile"
value="{{$ctrl.summary.mobile}}">
</vn-label-value>
<vn-label-value label="Email" no-ellipsize
value="{{$ctrl.listEmails($ctrl.summary.email)}}">
</vn-label-value>
<vn-label-value label="Sales person">
<span
<span
ng-click="workerDescriptor.show($event, $ctrl.summary.salesPersonFk)"
class="link">
{{$ctrl.summary.salesPersonUser.name}}
</span>
</vn-label-value>
<vn-label-value label="Channel"
<vn-label-value label="Channel"
value="{{$ctrl.summary.contactChannel.name}}">
</vn-label-value>
</vn-one>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.fiscalData({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Fiscal address</span>
</a>
</h4>
<h4
<h4
translate
ng-show="!$ctrl.isEmployee">
Fiscal address
</h4>
<vn-label-value label="Social name"
<vn-label-value label="Social name"
value="{{$ctrl.summary.socialName}}">
</vn-label-value>
<vn-label-value label="NIF / CIF"
<vn-label-value label="NIF / CIF"
value="{{$ctrl.summary.fi}}">
</vn-label-value>
<vn-label-value label="City"
<vn-label-value label="City"
value="{{$ctrl.summary.city}}">
</vn-label-value>
<vn-label-value label="Postcode"
<vn-label-value label="Postcode"
value="{{$ctrl.summary.postcode}}">
</vn-label-value>
<vn-label-value label="Province"
<vn-label-value label="Province"
value="{{$ctrl.summary.province.name}}">
</vn-label-value>
<vn-label-value label="Country"
<vn-label-value label="Country"
value="{{$ctrl.summary.country.country}}">
</vn-label-value>
<vn-label-value label="Street" no-ellipsize
@ -99,98 +102,98 @@
</vn-one>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.fiscalData({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Fiscal data</span>
</a>
</h4>
<h4
<h4
translate
ng-show="!$ctrl.isEmployee">
Fiscal data
</h4>
<vn-vertical>
<vn-check
label="Is equalizated"
ng-model="$ctrl.summary.isEqualizated"
label="Is equalizated"
ng-model="$ctrl.summary.isEqualizated"
disabled="true">
</vn-check>
<vn-check
label="Active"
label="Active"
ng-model="$ctrl.summary.isActive"
disabled="true">
</vn-check>
<vn-check
label="Invoice by address"
ng-model="$ctrl.summary.hasToInvoiceByAddress"
label="Invoice by address"
ng-model="$ctrl.summary.hasToInvoiceByAddress"
disabled="true">
</vn-check>
<vn-check
label="Verified data"
ng-model="$ctrl.summary.isTaxDataChecked"
label="Verified data"
ng-model="$ctrl.summary.isTaxDataChecked"
disabled="true">
</vn-check>
<vn-check
label="Has to invoice"
ng-model="$ctrl.summary.hasToInvoice"
label="Has to invoice"
ng-model="$ctrl.summary.hasToInvoice"
disabled="true">
</vn-check>
<vn-check
label="Notify by email"
ng-model="$ctrl.summary.isToBeMailed"
label="Notify by email"
ng-model="$ctrl.summary.isToBeMailed"
disabled="true">
</vn-check>
<vn-check
label="Vies"
ng-model="$ctrl.summary.isVies"
label="Vies"
ng-model="$ctrl.summary.isVies"
disabled="true">
</vn-check>
</vn-vertical>
</vn-one>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.billingData({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Billing data</span>
</a>
</h4>
<h4
<h4
translate
ng-show="!$ctrl.isEmployee">
Billing data
</h4>
<vn-label-value label="Pay method"
<vn-label-value label="Pay method"
value="{{$ctrl.summary.payMethod.name}}">
</vn-label-value>
<vn-label-value label="IBAN"
<vn-label-value label="IBAN"
value="{{$ctrl.summary.iban}}">
</vn-label-value>
<vn-label-value label="Due day"
<vn-label-value label="Due day"
value="{{$ctrl.summary.dueDay}}">
</vn-label-value>
<vn-vertical>
<vn-check
label="Received LCR"
ng-model="$ctrl.summary.hasLcr"
label="Received LCR"
ng-model="$ctrl.summary.hasLcr"
disabled="true">
</vn-check>
<vn-check
label="Received core VNL"
ng-model="$ctrl.summary.hasCoreVnl"
label="Received core VNL"
ng-model="$ctrl.summary.hasCoreVnl"
disabled="true">
</vn-check>
<vn-check
label="Received B2B VNL"
ng-model="$ctrl.summary.hasSepaVnl"
label="Received B2B VNL"
ng-model="$ctrl.summary.hasSepaVnl"
disabled="true">
</vn-check>
</vn-vertical>
</vn-one>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.address.index({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Address</span>
@ -201,10 +204,10 @@
ng-show="!$ctrl.isEmployee">
Address
</h4>
<vn-label-value label="Name"
<vn-label-value label="Name"
value="{{$ctrl.summary.defaultAddress.nickname}}">
</vn-label-value>
<vn-label-value label="City"
<vn-label-value label="City"
value="{{$ctrl.summary.defaultAddress.city}}">
</vn-label-value>
<vn-label-value label="Street" no-ellipsize
@ -213,17 +216,17 @@
</vn-one>
<vn-one>
<h4 ng-show="$ctrl.isEmployee">
<a
<a
ui-sref="client.card.webAccess({id:$ctrl.client.id})"
target="_self">
<span translate vn-tooltip="Go to">Web access</span>
</a>
</h4>
<h4
<h4
translate
ng-show="!$ctrl.isEmployee">Web access
</h4>
<vn-label-value label="User"
<vn-label-value label="User"
value="{{$ctrl.summary.account.name}}">
</vn-label-value>
<vn-vertical>
@ -236,52 +239,60 @@
</vn-one>
<vn-one>
<h4 translate>Business data</h4>
<vn-label-value label="Total greuge"
<vn-label-value label="Total greuge"
value="{{$ctrl.summary.totalGreuge | currency: 'EUR':2}}">
</vn-label-value>
<vn-label-value label="Mana"
<vn-label-value label="Mana"
value="{{$ctrl.summary.mana.mana | currency: 'EUR':2}}">
</vn-label-value>
<vn-label-value label="Rate"
<vn-label-value label="Rate"
value="{{$ctrl.claimRate($ctrl.summary.claimsRatio.priceIncreasing / 100) | percentage}}">
</vn-label-value>
<vn-label-value label="Average invoiced"
<vn-label-value label="Average invoiced"
value="{{$ctrl.summary.averageInvoiced.invoiced | currency: 'EUR':2}}">
</vn-label-value>
<vn-label-value label="Claims"
<vn-label-value label="Claims"
value="{{$ctrl.claimingRate($ctrl.summary.claimsRatio.claimingRate) | percentage}}">
</vn-label-value>
</vn-one>
<vn-one>
<h4 translate>Financial information</h4>
<vn-label-value label="Risk"
<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}"
info="Invoices minus payments plus orders not yet invoiced">
info="Invoices minus payments plus orders not yet invoiced">
</vn-label-value>
<vn-label-value label="Credit"
<vn-label-value label="Credit"
value="{{$ctrl.summary.credit | currency: 'EUR':2 }} "
ng-class="{alert: $ctrl.summary.credit > $ctrl.summary.creditInsurance ||
($ctrl.summary.credit && $ctrl.summary.creditInsurance == null)}"
info="Verdnatura's maximum risk">
</vn-label-value>
<vn-label-value label="Secured credit"
<vn-label-value label="Secured credit"
value="{{$ctrl.summary.creditInsurance | currency: 'EUR':2}} ({{$ctrl.summary.classifications[0].insurances[0].grade}})"
info="Solunion's maximum risk">
</vn-label-value>
<vn-label-value label="Balance"
<vn-label-value label="Balance"
value="{{$ctrl.summary.sumRisk | currency: 'EUR':2}}"
info="Invoices minus payments">
</vn-label-value>
<vn-label-value label="Balance due"
<vn-label-value label="Balance due"
value="{{($ctrl.summary.defaulters[0].amount >= 0 ? $ctrl.summary.defaulters[0].amount : '-') | currency: 'EUR':2}}"
ng-class="{alert: $ctrl.summary.defaulters[0].amount}"
info="Deviated invoices minus payments">
</vn-label-value>
<vn-label-value label="Recovery since"
<vn-label-value label="Recovery since"
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>
@ -341,7 +352,7 @@
class="link">
{{::ticket.refFk}}
</span>
<span
<span
ng-show="::!ticket.refFk"
class="chip {{::$ctrl.stateColor(ticket)}}">
{{::ticket.ticketState.state.name}}
@ -355,8 +366,8 @@
<vn-td actions>
<vn-icon-button
vn-anchor="::{
state: 'ticket.card.sale',
params: {id: ticket.id},
state: 'ticket.card.sale',
params: {id: ticket.id},
target: '_blank'
}"
vn-tooltip="Go to lines"
@ -386,10 +397,10 @@
<vn-route-descriptor-popover
vn-id="routeDescriptor">
</vn-route-descriptor-popover>
<vn-worker-descriptor-popover
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-invoice-out-descriptor-popover
<vn-invoice-out-descriptor-popover
vn-id="invoiceOutDescriptor">
</vn-invoice-out-descriptor-popover>
<vn-popup vn-id="summary">
@ -397,4 +408,4 @@
ticket="$ctrl.selectedTicket"
model="model">
</vn-ticket-summary>
</vn-popup>
</vn-popup>

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

@ -2,8 +2,13 @@
vn-client-summary .summary {
max-width: $width-lg;
.alert span {
color: $color-alert !important
}
}
vn-horizontal h4 .grafana:after {
content: 'contact_support';
font-size: 17px;
}
}

View File

@ -192,19 +192,19 @@
{{::buy.entryFk}}
</span>
</td>
<td number>{{::buy.buyingValue | currency: 'EUR':2}}</td>
<td number>{{::buy.freightValue | currency: 'EUR':2}}</td>
<td number>{{::buy.comissionValue | currency: 'EUR':2}}</td>
<td number>{{::buy.packageValue | currency: 'EUR':2}}</td>
<td number>{{::buy.buyingValue | currency: 'EUR':3}}</td>
<td number>{{::buy.freightValue | currency: 'EUR':3}}</td>
<td number>{{::buy.comissionValue | currency: 'EUR':3}}</td>
<td number>{{::buy.packageValue | currency: 'EUR':3}}</td>
<td>
<vn-check
disabled="true"
ng-model="::buy.isIgnored">
</vn-check>
</td>
<td number>{{::buy.price2 | currency: 'EUR':2}}</td>
<td number>{{::buy.price3 | currency: 'EUR':2}}</td>
<td number>{{::buy.minPrice | currency: 'EUR':2}}</td>
<td number>{{::buy.price2 | currency: 'EUR':3}}</td>
<td number>{{::buy.price3 | currency: 'EUR':3}}</td>
<td number>{{::buy.minPrice | currency: 'EUR':3}}</td>
<td>{{::buy.ektFk | dashIfEmpty}}</td>
<td>{{::buy.weight}}</td>
<td>{{::buy.packageFk}}</td>

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

@ -42,7 +42,7 @@
<vn-autocomplete
vn-one
ng-model="filter.requesterFk"
url="Workers/activeWithRole"
url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'salesPerson'}"

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: {
and: [
{originFk: id},
{action: 'update'},
{changedModel: 'Sale'}
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,
oldQuantity: oldQuantity || 0,
newQuantity: newQuantity || 0,
});
changes.push(message);
const changeMessage = $t('Change quantity', {
concept: log.changedModelValue,
oldQuantity: oldQuantity || 0,
newQuantity: newQuantity || 0,
});
changes.push(changeMessage);
}
}
}
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,41 +1,46 @@
<!DOCTYPE html>
<html>
<body>
<table class="mainTable">
<tbody>
<tr>
<td id="truck" class="ellipsize">{{labelData.truck || '---'}}</td>
</tr>
<tr>
<td>
<div v-html="getBarcode(labelData.palletFk)" id="barcode"></div>
<table v-for="labelData in labelsData" class="zoneTable">
<thead>
<tr v-if="!labelData.isMatch" id="black">
<td id="routeFk" class="ellipsize">{{labelData.routeFk}}</td>
<td id="zone" class="ellipsize">{{labelData.zone || '---'}}</td>
<td id="labels" class="ellipsize">{{labelData.labels}}</td>
</tr>
<tr v-else>
<td id="routeFk" class="ellipsize">{{labelData.routeFk}}</td>
<td id="zone" class="ellipsize">{{labelData.zone || '---'}}</td>
<td id="labels" class="ellipsize">{{labelData.labels || '--'}}</td>
</tr>
</thead>
</table>
</td>
</tr>
<tr>
<td>
<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>
<body>
<table class="mainTable">
<tbody>
<tr>
<td id="truck" class="ellipsize">{{labelData.truck || '---'}}</td>
</tr>
<tr>
<td>
<div v-html="getBarcode(labelData.palletFk)" id="barcode"></div>
<table v-for="labelData in labelsData" class="zoneTable">
<thead>
<tr v-if="!labelData.isMatch" id="black">
<td id="routeFk" class="ellipsize">{{labelData.routeFk}}</td>
<td id="zone" class="ellipsize">{{labelData.zone || '---'}}</td>
<td id="labels" class="ellipsize">{{labelData.labels}}</td>
</tr>
<tr v-else>
<td id="routeFk" class="ellipsize">{{labelData.routeFk}}</td>
<td id="zone" class="ellipsize">{{labelData.zone || '---'}}</td>
<td id="labels" class="ellipsize">{{labelData.labels || '--'}}</td>
</tr>
</thead>
</table>
</td>
</tr>
<tr>
<td>
<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 ? username.name : '---')}}
</div>
</td>
</tr>
</tbody>
</table>
</body>
<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);