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', type: 'Number',
description: 'The entity id', description: 'The entity id',
required: true required: true
}, }, {
{
arg: 'collection', arg: 'collection',
type: 'string', type: 'string',
description: 'The collection name', description: 'The collection name',
required: true required: true
}], }
],
returns: { returns: {
type: 'Object', type: 'Object',
root: true root: true

View File

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

View File

@ -53,7 +53,7 @@ async function test() {
const JunitReporter = require('jasmine-reporters'); const JunitReporter = require('jasmine-reporters');
jasmine.addReporter(new JunitReporter.JUnitXmlReporter()); jasmine.addReporter(new JunitReporter.JUnitXmlReporter());
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000;
jasmine.exitOnCompletion = true; 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`; DROP TABLE IF EXISTS `vn`.`dmsRecover`;
DROP PROCEDURE IF EXISTS `vn`.`route_getTickets`;
ALTER TABLE `vn`.`delivery` DROP COLUMN addressFk;
ALTER TABLE `vn`.`delivery` DROP CONSTRAINT delivery_ticketFk_FK;
ALTER TABLE `vn`.`delivery` DROP COLUMN ticketFk;
ALTER TABLE `vn`.`delivery` ADD ticketFk INT DEFAULT NULL;
ALTER TABLE `vn`.`delivery` ADD CONSTRAINT delivery_ticketFk_FK FOREIGN KEY (`ticketFk`) REFERENCES `vn`.`ticket`(`id`);
DROP PROCEDURE IF EXISTS vn.route_getTickets;
DELIMITER $$ DELIMITER $$
$$ $$
@ -17,14 +10,14 @@ BEGIN
* de sus tickets. * de sus tickets.
* *
* @param vRouteFk * @param vRouteFk
*
* @select Información de los tickets * @select Información de los tickets
*/ */
SELECT *
SELECT FROM (
t.id Id, SELECT t.id Id,
t.clientFk Client, t.clientFk Client,
a.id Address, a.id Address,
a.nickname ClientName,
t.packages Packages, t.packages Packages,
a.street AddressName, a.street AddressName,
a.postalCode PostalCode, a.postalCode PostalCode,
@ -37,34 +30,48 @@ BEGIN
d.longitude Longitude, d.longitude Longitude,
d.latitude Latitude, d.latitude Latitude,
wm.mediaValue SalePersonPhone, wm.mediaValue SalePersonPhone,
tob.Note Note, tob.description Note,
t.isSigned Signed t.isSigned Signed,
t.priority
FROM ticket t FROM ticket t
JOIN client c ON t.clientFk = c.id JOIN client c ON t.clientFk = c.id
JOIN address a ON t.addressFk = a.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 workerMedia wm ON wm.workerFk = c.salesPersonFk
LEFT JOIN LEFT JOIN(
(SELECT tob.description Note, t.id SELECT tob.description, t.id
FROM ticketObservation tob FROM ticketObservation tob
JOIN ticket t ON tob.ticketFk = t.id JOIN ticket t ON tob.ticketFk = t.id
JOIN observationType ot ON ot.id = tob.observationTypeFk JOIN observationType ot ON ot.id = tob.observationTypeFk
WHERE t.routeFk = vRouteFk WHERE t.routeFk = vRouteFk
AND ot.code = 'delivery' AND ot.code = 'delivery'
)tob ON tob.id = t.id )tob ON tob.id = t.id
LEFT JOIN LEFT JOIN(
(SELECT sub.ticketFk, SELECT sub.ticketFk,
CONCAT('(', GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk ORDER BY sub.items DESC SEPARATOR ','), ') ') itemPackingTypeFk CONCAT('(',
FROM (SELECT s.ticketFk , i.itemPackingTypeFk, COUNT(*) items GROUP_CONCAT(DISTINCT sub.itemPackingTypeFk
ORDER BY sub.items DESC SEPARATOR ','),
') ') itemPackingTypeFk
FROM (
SELECT s.ticketFk, i.itemPackingTypeFk, COUNT(*) items
FROM ticket t FROM ticket t
JOIN sale s ON s.ticketFk = t.id JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk JOIN item i ON i.id = s.itemFk
WHERE t.routeFk = vRouteFk WHERE t.routeFk = vRouteFk
GROUP BY t.id,i.itemPackingTypeFk)sub GROUP BY t.id, i.itemPackingTypeFk
)sub
GROUP BY sub.ticketFk GROUP BY sub.ticketFk
) sub2 ON sub2.ticketFk = t.id )sub2 ON sub2.ticketFk = t.id
WHERE t.routeFk = vRouteFk WHERE t.routeFk = vRouteFk
GROUP BY t.id ORDER BY d.id DESC
ORDER BY t.priority; LIMIT 10000000000000000000
)sub3
GROUP BY sub3.id
ORDER BY sub3.priority;
END$$ END$$
DELIMITER ; 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'), (1, 'First sector', 1, 1, 'FIRST'),
(2, 'Second sector', 2, 0, 'SECOND'); (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`) INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAddress`)
VALUES VALUES
(1, 'printer1', 'path1', 0, 1 , NULL), (1, 'printer1', 'path1', 0, 1 , NULL),
@ -1200,6 +1196,11 @@ INSERT INTO `vn`.`train`(`id`, `name`)
(1, 'Train1'), (1, 'Train1'),
(2, 'Train2'); (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`) INSERT INTO `vn`.`collection`(`id`, `workerFk`, `stateFk`, `created`, `trainFk`)
VALUES VALUES
(1, 1106, 5, DATE_ADD(util.VN_CURDATE(),INTERVAL +1 DAY), 1), (1, 1106, 5, DATE_ADD(util.VN_CURDATE(),INTERVAL +1 DAY), 1),

View File

@ -156,14 +156,14 @@ let actions = {
await this.waitForSpinnerLoad(); await this.waitForSpinnerLoad();
}, },
accessToSection: async function(state) { accessToSection: async function(state, name = 'Others') {
await this.waitForSelector('vn-left-menu'); await this.waitForSelector('vn-left-menu');
let nested = await this.evaluate(state => { let nested = await this.evaluate(state => {
return document.querySelector(`vn-left-menu li li > a[ui-sref="${state}"]`) != null; return document.querySelector(`vn-left-menu li li > a[ui-sref="${state}"]`) != null;
}, state); }, state);
if (nested) { 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 => { await this.evaluate(selector => {
document.querySelector(selector).scrollIntoViewIfNeeded(); document.querySelector(selector).scrollIntoViewIfNeeded();
}, selector); }, selector);

View File

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

View File

@ -21,8 +21,8 @@ module.exports = function(Self) {
let orgBeginTransaction = this.beginTransaction; let orgBeginTransaction = this.beginTransaction;
this.beginTransaction = function(options, cb) { this.beginTransaction = function(options, cb) {
options = options || {}; options = options || {};
if (!options.timeout) if (options.timeout === undefined)
options.timeout = 120000; options.timeout = 120 * 1000;
return orgBeginTransaction.call(this, options, cb); 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}}", "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", "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", "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", "Insert a date range": "Inserte un rango de fechas",
"Added observation": "{{user}} añadió esta observacion: {{text}}", "Added observation": "{{user}} añadió esta observacion: {{text}}",
"Comment added to client": "Observación añadida al cliente {{clientFk}}", "Comment added to client": "Observación añadida al cliente {{clientFk}}",

View File

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

View File

@ -17,6 +17,10 @@ class Controller extends Descriptor {
} }
sendPickupOrder() { 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`, { return this.vnEmail.send(`Claims/${this.claim.id}/claim-pickup-email`, {
recipient: this.claim.client.email, recipient: this.claim.client.email,
recipientId: this.claim.clientFk recipientId: this.claim.clientFk

View File

@ -20,3 +20,4 @@ Photos: Fotos
Go to the claim: Ir a la reclamación Go to the claim: Ir a la reclamación
Sale tracking: Líneas preparadas Sale tracking: Líneas preparadas
Ticket tracking: Estados del ticket 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, id,
params params
FROM clientConsumptionQueue FROM clientConsumptionQueue
WHERE status = ''`); WHERE status = '' OR status IS NULL`);
for (const queue of queues) { for (const queue of queues) {
try { try {
@ -44,6 +44,7 @@ module.exports = Self => {
GROUP BY c.id`, [params.clients, params.from, params.to]); GROUP BY c.id`, [params.clients, params.from, params.to]);
for (const client of clients) { for (const client of clients) {
try {
const args = { const args = {
id: client.clientFk, id: client.clientFk,
recipient: client.clientEmail, recipient: client.clientEmail,
@ -54,6 +55,12 @@ module.exports = Self => {
const email = new Email('campaign-metrics', args); const email = new Email('campaign-metrics', args);
await email.send(); await email.send();
} catch (error) {
if (error.code === 'EENVELOPE')
continue;
throw error;
}
} }
await Self.rawSql(` 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": { "ClientConsumptionQueue": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ClientInforma": {
"dataSource": "vn"
},
"ClientLog": { "ClientLog": {
"dataSource": "vn" "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/consumptionSendQueued')(Self);
require('../methods/client/filter')(Self); require('../methods/client/filter')(Self);
require('../methods/client/getClientOrSupplierReference')(Self); require('../methods/client/getClientOrSupplierReference')(Self);
require('../methods/client/setRating')(Self);
}; };

View File

@ -280,6 +280,10 @@ module.exports = Self => {
if (changes.credit !== undefined) if (changes.credit !== undefined)
await Self.changeCredit(ctx, finalState, changes); 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 = {}; const oldInstance = {};
if (!ctx.isNewInstance) { if (!ctx.isNewInstance) {
const newProps = Object.keys(changes); const newProps = Object.keys(changes);
@ -441,6 +445,19 @@ module.exports = Self => {
}, ctx.options); }, 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'); const app = require('vn-loopback/server/server');
app.on('started', function() { app.on('started', function() {
const VnUser = app.models.VnUser; const VnUser = app.models.VnUser;

View File

@ -141,6 +141,12 @@
}, },
"hasElectronicInvoice": { "hasElectronicInvoice": {
"type": "boolean" "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 './notification';
import './unpaid'; import './unpaid';
import './extended-list'; import './extended-list';
import './credit-management';

View File

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

View File

@ -23,6 +23,14 @@
{"state": "client.card.recovery.index", "icon": "icon-recovery"}, {"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"}, {"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"}, {"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", "description": "Others",
"icon": "more", "icon": "more",
@ -30,7 +38,6 @@
{"state": "client.card.sample.index", "icon": "mail"}, {"state": "client.card.sample.index", "icon": "mail"},
{"state": "client.card.consumption", "icon": "show_chart"}, {"state": "client.card.consumption", "icon": "show_chart"},
{"state": "client.card.mandate", "icon": "pan_tool"}, {"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.contact", "icon": "contact_phone"},
{"state": "client.card.webPayment", "icon": "icon-onlinepayment"}, {"state": "client.card.webPayment", "icon": "icon-onlinepayment"},
{"state": "client.card.dms.index", "icon": "cloud_upload"}, {"state": "client.card.dms.index", "icon": "cloud_upload"},
@ -416,7 +423,8 @@
"state": "client.notification", "state": "client.notification",
"component": "vn-client-notification", "component": "vn-client-notification",
"description": "Notifications" "description": "Notifications"
}, { },
{
"url": "/unpaid", "url": "/unpaid",
"state": "client.card.unpaid", "state": "client.card.unpaid",
"component": "vn-client-unpaid", "component": "vn-client-unpaid",
@ -428,6 +436,13 @@
"state": "client.extendedList", "state": "client.extendedList",
"component": "vn-client-extended-list", "component": "vn-client-extended-list",
"description": "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-crud-model
vn-id="ticketsModel" vn-id="ticketsModel"
url="Tickets" url="Tickets"
@ -253,7 +256,11 @@
</vn-label-value> </vn-label-value>
</vn-one> </vn-one>
<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" <vn-label-value label="Risk"
value="{{$ctrl.summary.debt.debt | currency: 'EUR':2}}" value="{{$ctrl.summary.debt.debt | currency: 'EUR':2}}"
ng-class="{alert: $ctrl.summary.debt.debt > $ctrl.summary.credit}" ng-class="{alert: $ctrl.summary.debt.debt > $ctrl.summary.credit}"
@ -282,6 +289,10 @@
ng-if="$ctrl.summary.recovery.started" ng-if="$ctrl.summary.recovery.started"
value="{{$ctrl.summary.recovery.started | date:'dd/MM/yyyy'}}"> value="{{$ctrl.summary.recovery.started | date:'dd/MM/yyyy'}}">
</vn-label-value> </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-one>
</vn-horizontal> </vn-horizontal>
<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 Deviated invoices minus payments: Facturas fuera de plazo menos recibos
Go to the client: Ir al cliente Go to the client: Ir al cliente
Latest tickets: Últimos tickets 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 { .alert span {
color: $color-alert !important color: $color-alert !important
} }
vn-horizontal h4 .grafana:after {
content: 'contact_support';
font-size: 17px;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,9 @@
"DeviceProduction": { "DeviceProduction": {
"dataSource": "vn" "dataSource": "vn"
}, },
"DeviceProductionLog": {
"dataSource": "vn"
},
"DeviceProductionModels": { "DeviceProductionModels": {
"dataSource": "vn" "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", "name": "DeviceProductionUser",
"base": "VnModel", "base": "Loggable",
"log": {
"model": "DeviceProductionLog",
"relation": "deviceProduction"
},
"options": { "options": {
"mysql": { "mysql": {
"table": "deviceProductionUser" "table": "deviceProductionUser"

View File

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

View File

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

View File

@ -1,6 +1,6 @@
span.separator{ @import "./variables";
border-left: 1px solid black;
height: 100%; .text-grey {
margin: 0 10px; 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 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. 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. 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 Los pedidos que se entregan por agencia o por reparto Verdnatura deben estar confirmados y pagados
y pagados antes de las 17h del día en que se preparan (el día anterior a recibirlos), antes del cierre de la correspondiente ruta el mismo día de preparación del pedido. Este horario
aunque esto puede variar si el pedido se envía a través de nuestro reparto y 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. según la zona.
howToPay: howToPay:
title: Cómo pagar 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,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body>
<body>
<table class="mainTable"> <table class="mainTable">
<tbody> <tbody>
<tr> <tr>
@ -27,15 +28,19 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<img :src="QR" id="QR"/> <img :src="QR" id="QR" />
<div id="right"> <div id="right">
<div id="additionalInfo" class="ellipsize"><b>Pallet: </b>{{id}}</div> <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>User: </b>
<div id="additionalInfo" class="ellipsize"><b>Day: </b>{{labelData.dayName.toUpperCase() || '---'}}</div> {{ (username ? username.name : '---')}}
</div>
<div id="additionalInfo" class="ellipsize"><b>Day: </b>{{labelData.dayName.toUpperCase() ||
'---'}}</div>
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</body> </body>
</html> </html>

View File

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