Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into test
gitea/salix/test This commit looks good Details

This commit is contained in:
Joan Sanchez 2019-11-14 14:25:26 +01:00
commit 35565cc287
42 changed files with 238 additions and 761 deletions

View File

@ -35,9 +35,13 @@ CREATE TABLE `vn`.`zoneExclusion` (
KEY `zoneFk` (`zoneFk`), KEY `zoneFk` (`zoneFk`),
CONSTRAINT `zoneExclusion_ibfk_1` FOREIGN KEY (`zoneFk`) REFERENCES `zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT `zoneExclusion_ibfk_1` FOREIGN KEY (`zoneFk`) REFERENCES `zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
ALTER TABLE `vn`.`zone` ALTER TABLE `vn`.`zone`
DROP FOREIGN KEY `fk_zone_1`; DROP FOREIGN KEY `fk_zone_1`;
ALTER TABLE `vn`.`zone` ALTER TABLE `vn`.`zone`
DROP COLUMN `warehouseFk`, CHANGE COLUMN `warehouseFk` `warehouseFk` SMALLINT(6) UNSIGNED NULL DEFAULT NULL ;
DROP INDEX `fk_zone_1_idx`; ALTER TABLE `vn`.`zone`
ADD CONSTRAINT `fk_zone_1`
FOREIGN KEY (`warehouseFk`)
REFERENCES `vn`.`warehouse` (`id`)
ON DELETE NO ACTION
ON UPDATE CASCADE;

View File

@ -5,25 +5,14 @@ describe('buyUltimateFromInterval()', () => {
let today; let today;
let future; let future;
beforeAll(() => { beforeAll(() => {
let date = new Date(); let now = new Date();
let month = `${date.getMonth() + 1}`; now.setHours(0, 0, 0, 0);
let futureMonth = `${date.getMonth() + 2}`; today = now;
let day = date.getDate();
let year = date.getFullYear();
let futureYear = year;
if (month.toString().length < 2) month = '0' + month; let futureDate = new Date(now);
if (futureMonth.toString().length < 2) futureMonth = '0' + futureMonth; let futureMonth = now.getMonth() + 1;
if (futureMonth.toString() == '13') { futureDate.setMonth(futureMonth);
futureMonth = '01'; future = futureDate;
futureYear + 1;
}
if (day.toString().length < 2) day = `0${day}`;
today = [year, month, day].join('-');
future = [futureYear, futureMonth, day].join('-');
}); });
it(`should create a temporal table with it's data`, async() => { it(`should create a temporal table with it's data`, async() => {
@ -65,8 +54,8 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3); expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5); expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
}); });
it(`should create a temporal table with it's data in which started value is assigned to ended`, async() => { it(`should create a temporal table with it's data in which started value is assigned to ended`, async() => {
@ -101,8 +90,8 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3); expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5); expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
}); });
it(`should create a temporal table with it's data in which ended value is a date in the future`, async() => { it(`should create a temporal table with it's data in which ended value is a date in the future`, async() => {
@ -137,7 +126,7 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3); expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5); expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today)); expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
}); });
}); });

View File

@ -111,9 +111,7 @@
"This phone already exists": "Este teléfono ya existe", "This phone already exists": "Este teléfono ya existe",
"You cannot move a parent to its own sons": "No puedes mover un elemento padre a uno de sus hijos", "You cannot move a parent to its own sons": "No puedes mover un elemento padre a uno de sus hijos",
"You can't create a claim for a removed ticket": "No puedes crear una reclamación para un ticket eliminado", "You can't create a claim for a removed ticket": "No puedes crear una reclamación para un ticket eliminado",
"You cannot delete this ticket because is already invoiced, deleted or prepared": "No puedes eliminar este tiquet porque ya está facturado, eliminado o preparado",
"You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada", "You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada",
"You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero", "You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero",
"Has deleted the ticket id": "Ha eliminado el ticket id [#{{id}}]({{{url}}})", "Has deleted the ticket id": "Ha eliminado el ticket id [#{{id}}]({{{url}}})"
"You cannot remove this ticket because is already invoiced, deleted or prepared": "You cannot remove this ticket because is already invoiced, deleted or prepared"
} }

View File

@ -0,0 +1,34 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('calculate', {
description: 'Calculates the price of a sale and its components',
accessType: 'WRITE',
accepts: [{
arg: 'id',
description: 'The sale id',
type: 'number',
required: true,
http: {source: 'path'}
}],
returns: {
type: 'Number',
root: true
},
http: {
path: `/:id/calculate`,
verb: 'post'
}
});
Self.calculate = async(ctx, id) => {
const models = Self.app.models;
const sale = await Self.findById(id);
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
return Self.rawSql('CALL vn.ticketCalculateSale(?)', [id]);
};
};

View File

@ -0,0 +1,24 @@
const app = require('vn-loopback/server/server');
describe('sale calculate()', () => {
const saleId = 7;
it('should update the sale price', async() => {
const ctx = {req: {accessToken: {userId: 9}}};
const response = await app.models.Sale.calculate(ctx, saleId);
expect(response.affectedRows).toBeDefined();
});
it('should throw an error if the ticket is not editable', async() => {
const ctx = {req: {accessToken: {userId: 9}}};
const immutableSaleId = 1;
await app.models.Sale.calculate(ctx, immutableSaleId)
.catch(response => {
expect(response).toEqual(new Error(`The sales of this ticket can't be modified`));
error = response;
});
expect(error).toBeDefined();
});
});

View File

@ -27,7 +27,7 @@ module.exports = Self => {
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
if (!isEditable) if (!isEditable)
throw new UserError('You cannot delete this ticket because is already invoiced, deleted or prepared'); throw new UserError(`The sales of this ticket can't be modified`);
// Check if has sales with shelving // Check if has sales with shelving
const sales = await models.Sale.find({ const sales = await models.Sale.find({

View File

@ -5,6 +5,7 @@ module.exports = Self => {
require('../methods/sale/updatePrice')(Self); require('../methods/sale/updatePrice')(Self);
require('../methods/sale/updateQuantity')(Self); require('../methods/sale/updateQuantity')(Self);
require('../methods/sale/updateConcept')(Self); require('../methods/sale/updateConcept')(Self);
require('../methods/sale/calculate')(Self);
Self.validatesPresenceOf('concept', { Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank` message: `Concept cannot be blank`

View File

@ -9,6 +9,5 @@
on-step-end="$ctrl.onSubmit()"> on-step-end="$ctrl.onSubmit()">
</vn-step-control> </vn-step-control>
</vn-button-bar> </vn-button-bar>
<div compact>
<ui-view></ui-view> <ui-view></ui-view>
</div>

View File

@ -5,7 +5,7 @@
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<form name="form"> <form name="form">
<vn-card class="vn-pa-lg"> <vn-card class="vn-w-md vn-pa-lg">
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-one <vn-autocomplete vn-one
vn-id="client" vn-id="client"

View File

@ -1,5 +1,5 @@
<form name="form"> <form name="form">
<vn-card class="vn-pa-lg"> <vn-card class="vn-w-md vn-pa-lg">
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-one <vn-autocomplete vn-one
url="TicketUpdateActions" url="TicketUpdateActions"

View File

@ -13,8 +13,14 @@ class Controller {
this.data.registerChild(this); this.data.registerChild(this);
} }
$onChanges() { get ticket() {
this.ticket.option = 1; return this._ticket;
}
set ticket(value) {
this._ticket = value;
if (value) this.ticket.option = 1;
} }
onStepChange(state) { onStepChange(state) {

View File

@ -21,8 +21,7 @@ describe('ticket', () => {
describe('onSubmit()', () => { describe('onSubmit()', () => {
it(`should return an error if the item doesn't have option property in the controller`, () => { it(`should return an error if the item doesn't have option property in the controller`, () => {
controller.ticket = {}; controller._ticket = {id: 1};
controller.onSubmit(); controller.onSubmit();
expect(vnApp.showError).toHaveBeenCalledWith('Choose an option'); expect(vnApp.showError).toHaveBeenCalledWith('Choose an option');

View File

@ -1,42 +1,40 @@
<form name="form"> <form name="form">
<vn-card class="vn-pa-lg"> <vn-card class="vn-w-lg vn-pa-lg">
<vn-horizontal> <vn-table>
<table class="vn-table"> <vn-thead>
<thead> <vn-tr>
<tr> <vn-th number>Item</vn-th>
<th number translate>Item</th> <vn-th style="text-align:center">Description</vn-th>
<th translate style="text-align:center">Description</th> <vn-th number>Quantity</vn-th>
<th number translate>Quantity</th> <vn-th number>Price (PPU)</vn-th>
<th number translate>Price (PPU)</th> <vn-th number>New price (PPU)</vn-th>
<th number translate>New price (PPU)</th> <vn-th number>Price difference</vn-th>
<th number translate>Price difference</th> </vn-tr>
</tr> </vn-thead>
</thead> <vn-tbody>
<tbody> <vn-tr ng-repeat="sale in $ctrl.ticket.sale.items track by sale.id">
<tr ng-repeat="sale in $ctrl.ticket.sale.items track by sale.id"> <vn-td number>{{("000000"+sale.itemFk).slice(-6)}}</vn-td>
<td number>{{("000000"+sale.itemFk).slice(-6)}}</td> <vn-td expand>
<td expand>
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"> name="::sale.concept">
</vn-fetched-tags> </vn-fetched-tags>
</td> </vn-td>
<td number>{{::sale.quantity}}</td> <vn-td number>{{::sale.quantity}}</vn-td>
<td number>{{::sale.price | currency: 'EUR': 2}}</td> <vn-td number>{{::sale.price | currency: 'EUR': 2}}</vn-td>
<td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</td> <vn-td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</vn-td>
<td number>{{::sale.component.difference | currency: 'EUR': 2}}</td> <vn-td number>{{::sale.component.difference | currency: 'EUR': 2}}</vn-td>
</tr> </vn-tr>
</tbody> </vn-tbody>
<tfoot> <vn-tfoot>
<tr> <vn-tr>
<td colspan="3"></td> <vn-td colspan="3"></vn-td>
<td number><strong>{{$ctrl.totalPrice | currency: 'EUR': 2}}</strong></td> <vn-td number><strong>{{$ctrl.totalPrice | currency: 'EUR': 2}}</strong></vn-td>
<td number><strong>{{$ctrl.totalNewPrice | currency: 'EUR': 2}}</strong></td> <vn-td number><strong>{{$ctrl.totalNewPrice | currency: 'EUR': 2}}</strong></vn-td>
<td number><strong>{{$ctrl.totalPriceDifference | currency: 'EUR': 2}}</strong></td> <vn-td number><strong>{{$ctrl.totalPriceDifference | currency: 'EUR': 2}}</strong></vn-td>
</tr> </vn-tr>
</tfoot> </vn-tfoot>
</table> </vn-table>
</vn-horizontal>
</vn-card> </vn-card>
</form> </form>

View File

@ -7,6 +7,17 @@ class Controller {
$onInit() { $onInit() {
this.data.registerChild(this); this.data.registerChild(this);
}
get ticket() {
return this._ticket;
}
set ticket(value) {
this._ticket = value;
if (!value) return;
this.getTotalPrice(); this.getTotalPrice();
this.getTotalNewPrice(); this.getTotalNewPrice();
this.getTotalDifferenceOfPrice(); this.getTotalDifferenceOfPrice();

View File

@ -40,7 +40,7 @@ class Controller extends Component {
showChangeShipped() { showChangeShipped() {
if (!this.isEditable) { if (!this.isEditable) {
this.vnApp.showError(this.$translate.instant('This ticket can\'t be modified')); this.vnApp.showError(this.$translate.instant(`This ticket can't be modified`));
return; return;
} }
this.newShipped = this.ticket.shipped; this.newShipped = this.ticket.shipped;

View File

@ -32,6 +32,11 @@ class Controller {
callback: this.createClaim, callback: this.createClaim,
show: () => this.isEditable show: () => this.isEditable
}, },
{
name: 'Recalculate price',
callback: this.calculateSalePrice,
show: () => this.hasOneSaleSelected()
},
]; ];
this._sales = []; this._sales = [];
this.imagesPath = '//verdnatura.es/vn-image-data/catalog'; this.imagesPath = '//verdnatura.es/vn-image-data/catalog';
@ -534,6 +539,21 @@ class Controller {
this.isEditable = res.data; this.isEditable = res.data;
}); });
} }
hasOneSaleSelected() {
if (this.totalCheckedLines() === 1)
return true;
return false;
}
calculateSalePrice() {
const sale = this.checkedLines()[0];
const query = `Sales/${sale.id}/calculate`;
this.$http.post(query).then(res => {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$scope.model.refresh();
});
}
} }
Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate']; Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate'];

View File

@ -30,3 +30,4 @@ SMSAvailability: >-
Continue anyway?: ¿Continuar de todas formas? Continue anyway?: ¿Continuar de todas formas?
This ticket is now empty: El ticket ha quedado vacio This ticket is now empty: El ticket ha quedado vacio
Do you want to delete it?: ¿Quieres borrarlo? Do you want to delete it?: ¿Quieres borrarlo?
Recalculate price: Recalcular precio

View File

@ -1,5 +1,6 @@
import '../index.js'; import '../index.js';
import watcher from 'core/mocks/watcher'; import watcher from 'core/mocks/watcher';
import crudModel from 'core/mocks/crud-model';
describe('Ticket', () => { describe('Ticket', () => {
describe('Component vnTicketSale', () => { describe('Component vnTicketSale', () => {
@ -40,6 +41,7 @@ describe('Ticket', () => {
$scope.watcher = watcher; $scope.watcher = watcher;
$scope.sms = {open: () => {}}; $scope.sms = {open: () => {}};
$scope.ticket = ticket; $scope.ticket = ticket;
$scope.model = crudModel;
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
Object.defineProperties($state.params, { Object.defineProperties($state.params, {
id: { id: {
@ -334,5 +336,27 @@ describe('Ticket', () => {
expect(window.open).toHaveBeenCalledWith('/somePath', '_blank'); expect(window.open).toHaveBeenCalledWith('/somePath', '_blank');
}); });
}); });
describe('hasOneSaleSelected()', () => {
it('should return true if just one sale is selected', () => {
controller.sales[0].checked = true;
expect(controller.hasOneSaleSelected()).toBeTruthy();
});
});
describe('calculateSalePrice()', () => {
it('should make an HTTP post query ', () => {
controller.sales[0].checked = true;
$httpBackend.when('POST', `Sales/4/calculate`).respond(200);
$httpBackend.whenGET(`Tickets/1/subtotal`).respond(200, 227.5);
$httpBackend.whenGET(`Tickets/1/getVAT`).respond(200, 10.5);
$httpBackend.whenGET(`Tickets/1/isEditable`).respond();
controller.calculateSalePrice();
$httpBackend.flush();
});
});
}); });
}); });

View File

@ -1,170 +0,0 @@
/*
Author : Enrique Blasco BLanquer
Date: 29 de octubre de 2019
*/
let request = require('request-promise-native');
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('sendMessage', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [{
arg: 'from',
type: 'String',
required: true,
description: 'user who sends the message'
}, {
arg: 'to',
type: 'String',
required: true,
description: 'user (@) or channel (#) to send the message'
}, {
arg: 'message',
type: 'String',
required: true,
description: 'The message'
}],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/sendMessage`,
verb: 'POST'
}
});
Self.sendMessage = async(from, to, message) => {
const rocketUser = await getRocketUser();
const userId = rocketUser.data.userId;
const authToken = rocketUser.data.authToken;
if (to.includes('@')) return await sendUserMessage(to.replace('@', ''), userId, authToken, '@' + from + ' te ha mandado un mensaje: ' + message);
else return await sendChannelMessage(to.replace('#', ''), userId, authToken, '@' + from + ' dice: ' + message);
};
/**
* Returns a rocketchat token
* @return {Object} userId and authToken
*/
async function getRocketUser() {
const url = 'https://chat.verdnatura.es/api/v1/login';
const options = {
method: 'POST',
uri: url,
body: {
user: 'VnBot',
password: 'Ub606cux7op.'
},
headers: {
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Send a user message
* @param {String} to user to send the message
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @param {String} message The message
* @return {Object} rocket info
*/
async function sendUserMessage(to, userId, authToken, message) {
const url = 'https://chat.verdnatura.es/api/v1/chat.postMessage';
const options = {
method: 'POST',
uri: url,
body: {
'channel': '@' + to,
'text': message
},
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Send a channel message
* @param {String} to channel to send the message
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @param {String} message The message
* @return {Object} rocket info
*/
async function sendChannelMessage(to, userId, authToken, message) {
const channelInfo = await getChannelId(to, userId, authToken);
const url = 'https://chat.verdnatura.es/api/v1/chat.sendMessage';
const channelId = channelInfo.channel._id;
const options = {
method: 'POST',
uri: url,
body: {
'message': {
'rid': channelId,
'msg': message
}
},
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Get channel id
* @param {String} to channel to get id
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @return {Object} rocket info
*/
async function getChannelId(to, userId, authToken) {
const url = 'https://chat.verdnatura.es/api/v1/channels.info?roomName=' + to;
const options = {
method: 'GET',
uri: url,
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
};

View File

@ -3,5 +3,4 @@ module.exports = Self => {
require('../methods/worker/mySubordinates')(Self); require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self); require('../methods/worker/isSubordinate')(Self);
require('../methods/worker/getWorkerInfo')(Self); require('../methods/worker/getWorkerInfo')(Self);
require('../methods/worker/sendMessage')(Self);
}; };

View File

@ -4,6 +4,6 @@
<section v-if="centerText" class="uppercase">{{centerText}}</section> <section v-if="centerText" class="uppercase">{{centerText}}</section>
<section class="number">{{$t('numPages')}}</section> <section class="number">{{$t('numPages')}}</section>
</section> </section>
<p class="phytosanitary">{{$t('law.phytosanitary')}}</p> <p class="phytosanitary" v-if="showPhytosanitary">{{$t('law.phytosanitary')}}</p>
<p class="privacy" v-html="$t('law.privacy')"></p> <p class="privacy" v-html="$t('law.privacy')"></p>
</footer> </footer>

View File

@ -1,4 +1,4 @@
module.exports = { module.exports = {
name: 'report-footer', name: 'report-footer',
props: ['leftText', 'centerText', 'locale'] props: ['leftText', 'centerText', 'locale', 'showPhytosanitary']
}; };

View File

@ -1,7 +0,0 @@
const CssReader = require(`${appPath}/lib/cssReader`);
module.exports = new CssReader([
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`,
`${appPath}/common/css/misc.css`])
.mergeStyles();

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<title>{{ $t('subject') }}</title>
</head>
<body>
<section class="container">
<!-- Header component -->
<email-header></email-header>
<!-- End header component -->
<section class="main">
<!-- Title block -->
<div class="title">
<h1>{{ $t('title') }}</h1>
</div>
<!-- Title block end -->
<p>{{$t('description.dear')}},</p>
<p>{{$t('description.instructions')}}</p>
<p>{{$t('description.conclusion')}}</p>
</section>
<!-- Footer component -->
<email-footer :locale="locale"></email-footer>
<!-- End footer component -->
</section>
</body>
</html>

View File

@ -1,49 +0,0 @@
const database = require(`${appPath}/lib/database`);
const reportEngine = require(`${appPath}/lib/reportEngine.js`);
const UserException = require(`${appPath}/lib/exceptions/userException`);
module.exports = {
name: 'client-lcr',
async asyncData(ctx, params) {
const promises = [];
const data = {
isPreview: ctx.method === 'GET',
};
if (!params.clientFk)
throw new UserException('No client id specified');
promises.push(reportEngine.toPdf('rpt-lcr', ctx));
promises.push(this.methods.fetchClient(params.clientFk));
return Promise.all(promises).then(result => {
const stream = result[0];
const [[client]] = result[1];
Object.assign(data, client);
Object.assign(data, {attachments: [{filename: 'rpt-lcr.pdf', content: stream}]});
return data;
});
},
created() {
if (this.locale)
this.$i18n.locale = this.locale;
},
methods: {
fetchClient(clientFk) {
return database.pool.query(`
SELECT
u.lang locale,
c.email recipient
FROM client c
JOIN account.user u ON u.id = c.id
WHERE c.id = ?`, [clientFk]);
},
},
components: {
'email-header': require('../email-header'),
'email-footer': require('../email-footer'),
},
};

View File

@ -1,64 +0,0 @@
module.exports = {
messages: {
es: {
subject: 'Autorisation pour débit',
title: 'Autorisation pour débit',
description: {
dear: 'Messieurs',
instructions: `Étant donné les excellentes relations existantes entre nos
deux sociétés et en vue de faciliter les processus de
paiement de nos factures, nous vous suggérons l'utilisation
du système française de compensation LCR.
Ce service consiste à effectuer des recouvrements
automatiques, de manière électronique, de nos effets -
lettres de change et billets à ordre - tirés sur votre société
en Euro, qui présente comme principal avantage pour vous
la substantielle réduction de coûts dans des termes de frais
et commissions bancaires.
Dans le cas vous accepteriez notre proposition, à
léchéance de chaque effet, votre compte sera débité
automatiquement par votre Banque.
Ainsi, nous vous demandons de signer et envoyer à votre
Banque l'original de l'autorisation pour débit en annexe,
dûment remplie, et de nous retourner une photocopie de la
dite autorisation.
Ce système étant basé sur la transmission de données de
manière électronique, le maniement de documents
physiques á été éliminé
En vous remercieront pour votre collaboration, nous vous
prions dagréer, Messieurs, nos salutations distinguées.`,
conclusion: 'Bien cordialement'
},
},
fr: {
subject: 'Autorisation pour débit',
title: 'Autorisation pour débit',
description: {
dear: 'Messieurs',
instructions: `Étant donné les excellentes relations existantes entre nos
deux sociétés et en vue de faciliter les processus de
paiement de nos factures, nous vous suggérons l'utilisation
du système française de compensation LCR.
Ce service consiste à effectuer des recouvrements
automatiques, de manière électronique, de nos effets -
lettres de change et billets à ordre - tirés sur votre société
en Euro, qui présente comme principal avantage pour vous
la substantielle réduction de coûts dans des termes de frais
et commissions bancaires.
Dans le cas vous accepteriez notre proposition, à
léchéance de chaque effet, votre compte sera débité
automatiquement par votre Banque.
Ainsi, nous vous demandons de signer et envoyer à votre
Banque l'original de l'autorisation pour débit en annexe,
dûment remplie, et de nous retourner une photocopie de la
dite autorisation.
Ce système étant basé sur la transmission de données de
manière électronique, le maniement de documents
physiques á été éliminé
En vous remercieront pour votre collaboration, nous vous
prions dagréer, Messieurs, nos salutations distinguées.`,
conclusion: 'Bien cordialement'
},
},
},
};

View File

@ -27,9 +27,11 @@
<div class="grid-row"> <div class="grid-row">
<div class="grid-block white vn-pa-lg"> <div class="grid-block white vn-pa-lg">
<h1>{{ $t('title') }}</h1> <h1>{{ $t('title') }}</h1>
<p>{{$t('dearClient')}},</p> <p>{{$t('dear')}},</p>
<p v-html="$t('clientData')"></p> <p v-html="$t('description', [ticketId])"></p>
<p v-html="$t('poll')"></p>
<p v-html="$t('help')"></p> <p v-html="$t('help')"></p>
<p v-html="$t('conclusion')"></p>
</div> </div>
</div> </div>
<!-- Footer block --> <!-- Footer block -->

View File

@ -1,6 +1,10 @@
subject: Aquí tienes tu albarán subject: Aquí tienes tu albarán
title: "¡Este es tu albarán!" title: "¡Este es tu albarán!"
dearClient: Estimado cliente dear: Estimado cliente
clientData: A continuación adjuntamos tu albarán. description: Ya está disponible el albarán correspondiente al pedido {0}. <br/>
Puedes descargarlo haciendo clic en el adjunto de este correo.
poll: Si lo deseas, puedes responder a nuestra encuesta de satisfacción para
ayudarnos a prestar un mejor servicio. ¡Tu opinión es muy importante para nosotros!
help: Cualquier duda que te surja, no dudes en consultarla, <strong>¡estamos para help: Cualquier duda que te surja, no dudes en consultarla, <strong>¡estamos para
atenderte!</strong> atenderte!</strong>
conclusion: ¡Gracias por tu atención!

View File

@ -0,0 +1,9 @@
subject: Voici votre bon de livraison
title: "Voici votre bon de livraison!"
dear: Cher client,
description: Le bon de livraison correspondant à la commande {0} est maintenant disponible.<br/>
Vous pouvez le télécharger en cliquant sur la pièce jointe dans cet email.
poll: Si vous le souhaitez, vous pouvez répondre à notre questionaire de satisfaction
pour nous aider à améliorer notre service. Votre avis est très important pour nous!
help: N'hésitez pas nous envoyer toute doute ou question, <strong>nous sommes là pour vous aider!</strong>
conclusion: Merci pour votre attention!

View File

@ -2,7 +2,17 @@ subject: Solicitud de domiciliación bancaria
title: Domiciliación SEPA CORE title: Domiciliación SEPA CORE
description: description:
dear: Estimado cliente dear: Estimado cliente
instructions: Para poder tramitar tu solicitud de cambio de tu forma de pago a giro instructions: <p>Dadas las excelentes relaciones existentes entre nuestras
bancario, te adjuntamos los documentos correspondientes a la ley de pago, que dos empresas y para facilitar los procesos de pago de nuestras facturas,
tienes que cumplimentar y enviarnos. sugerimos el uso del sistema de domiciliación bancaria SEPA CORE.</p>
conclusion: Gracias por tu atención. <p>Este servicio consiste en emitir nuestros recibos a su empresa de
forma automatizada y electrónicamente, lo que supone para usted una reducción
sustancial de costos en términos de honorarios y gastos bancarios.</p>
<p>En caso de que acepte nuestra propuesta, a la fecha de vencimiento de cada efecto,
se debitará a su cuenta automáticamente a través de su entidad bancaria.
Por tanto, le pedimos que firme y envíe a su banco la autorización original adjunta,
debidamente cumplimentada, y nos devuelva una fotocopia de dicha autorización.</p>
<p>Este sistema se basa en la transmisión electrónica de datos;
el manejo de documentos físicos ha sido eliminado.</p>
<p>Le agradecemos su cooperación,</p>
conclusion: ¡Gracias por su atención!

View File

@ -238,6 +238,7 @@
<div class="grid-row"> <div class="grid-row">
<div class="grid-block"> <div class="grid-block">
<report-footer id="pageFooter" <report-footer id="pageFooter"
v-bind:show-phytosanitary="true"
v-bind:left-text="$t('ticket', [ticket.id])" v-bind:left-text="$t('ticket', [ticket.id])"
v-bind:center-text="client.socialName" v-bind:center-text="client.socialName"
v-bind:is-preview="isPreview" v-bind:is-preview="isPreview"

View File

@ -1,8 +0,0 @@
const CssReader = require(`${appPath}/lib/cssReader`);
module.exports = new CssReader([
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${appPath}/common/css/misc.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -1,31 +0,0 @@
.payment-type {
width: auto
}
.payment-type th:nth-child(2), .payment-type th:nth-child(5) {
padding: 10px !important
}
.payment-type th:nth-child(3){
padding: 0 50px !important
}
.table-margin {
margin-top: 20px
}
.grey-background {
background-color: #DDD
}
.emptyField {
width: 100%;
}
.row-oriented.input-table > tbody > tr > td {
width: 10% !important
}
.row-oriented.input-table > tbody > tr > th {
width: 90% !important
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View File

@ -1,189 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<body>
<section class="container">
<!-- Header component -->
<report-header :locale="locale"></report-header>
<!-- End header component -->
<section class="main">
<h1 class="title centered">{{$t('title')}}</h1>
<section class="panel">
<section class="header">{{$t('Creditor')}}</section>
<section class="body">
<table class="row-oriented">
<tbody>
<tr>
<td >{{$t('supplier.name')}}:</td>
<th>{{supplierName}}</th>
</tr>
<tr>
<td>{{$t('supplier.street')}}:</td>
<th>{{supplierStreet}}</th>
</tr>
<tr>
<td></td>
<th>{{supplierPostCode}}, {{supplierCity}} ({{supplierProvince}})</th>
</tr>
<tr>
<td></td>
<th>{{supplierCountry}}</th>
</tr>
</tbody>
</table>
</section>
</section>
<section class="panel">
<section class="header">{{$t('Deptor')}}</section>
<section class="body">
<table class="row-oriented">
<tbody>
<tr>
<td>{{$t('client.name')}}:</td>
<th>{{clientName}}</th>
</tr>
<tr>
<td>{{$t('client.street')}}:</td>
<th>{{clientStreet}}</th>
</tr>
<tr>
<td></td>
<th>{{clientPostCode}}, {{clientCity}} ({{clientProvince}})</th>
</tr>
<tr>
<td></td>
<th>{{clientCountry}}</th>
</tr>
<tr>
<td>{{$t('client.fi')}}:</td>
<th>
<section class="field square">
<span v-for="i in 12">{{fi.charAt(i)}}</span>
</section>
</th>
</tr>
</tbody>
</table>
</section>
</section>
<p class="font">{{$t('description')}}</p>
<section class="panel">
<section class="header">{{$t('Bank')}}</section>
<section class="body">
<section class="vertical-text">
{{$t('client.toCompleteByClient')}}
</section>
<table class="row-oriented input-table">
<tbody>
<tr>
<td>{{$t('bank.name')}}:</td>
<th><span class="emptyField"></span></th>
</tr>
<tr>
<td>{{$t('bank.street')}}:</td>
<th><span class="emptyField"></span></th>
</tr>
</tbody>
</table>
<!-- RIB -->
<table class="table-margin">
<tbody>
<tr>
<td>{{$t('bank.account')}}:</td>
</tr>
<tr>
<td style="padding-right: 1em">
<section class="field square">
<span v-for="i in 5"></span>
</section>
</td>
<td style="padding-right: 1em">
<section class="field square">
<span v-for="i in 5"></span>
</section>
</td>
<td style="padding-right: 1em">
<section class="field square">
<span v-for="i in 11"></span>
</section>
</td>
<td>
<section class="field square" >
<span v-for="i in 2"></span>
</section>
</td>
</tr>
<tr>
<td style="padding-right: 1em">
<div class="line">
<div class="vertical-aligned">
<span>{{$t('bank.bankCode')}}</span>
</div>
</div>
</td>
<td style="padding-right: 1em">
<div class="line">
<div class="vertical-aligned">
<span>{{$t('bank.agencyCode')}}</span>
</div>
</div>
</td>
<td style="padding-right: 1em">
<div class="line">
<div class="vertical-aligned">
<span>{{$t('bank.accountNumber')}}</span>
</div>
</div>
</td>
<td>
<div class="line">
<div class="vertical-aligned">
<span>{{$t('bank.ribKey')}}</span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<!-- IBAN -->
<table class="table-margin">
<tbody>
<tr>
<td>IBAN:</td>
</tr>
<tr>
<td>
<section class="field square">
<span class="grey-background">F</span>
<span class="grey-background">R</span>
<span v-for="i in 23"></span>
</section>
</td>
</tr>
</tbody>
</table>
</section>
</section>
<p>{{$t('authorization')}}</p>
<!-- signature -->
<section class="signature panel">
<section class="header">{{$t('client.sign')}}</section>
<section class="body centered">
<section>
<p>{{$t('client.signDate')}}:</p>
</section>
</section>
</section>
</section>
<!-- Footer component -->
<report-footer id="pageFooter"
:left-text="$t('order', [mandateCode])"
:center-text="clientName"
:locale="locale">
</report-footer>
<!-- End footer component -->
</section>
</body>
</html>

View File

@ -1,80 +0,0 @@
const strftime = require('strftime');
const database = require(`${appPath}/lib/database`);
const UserException = require(`${appPath}/lib/exceptions/userException`);
module.exports = {
name: 'rpt-lcr',
async asyncData(ctx, params) {
if (!params.clientFk)
throw new UserException('No client id specified');
if (!params.companyFk)
throw new UserException('No company id specified');
return this.methods.fetchClient(params.clientFk, params.companyFk)
.then(([[client]]) => {
if (!client)
throw new UserException('No client data found');
return client;
});
},
created() {
if (this.locale)
this.$i18n.locale = this.locale;
const embeded = [];
this.files.map(file => {
embeded[file] = `file://${__dirname + file}`;
});
this.embeded = embeded;
},
data() {
return {
files: ['/assets/images/signature.png']
};
},
methods: {
fetchClient(clientFk, companyFk) {
return database.pool.query(
`SELECT
c.id clientId,
u.lang locale,
m.code mandateCode,
c.socialName AS clientName,
c.street AS clientStreet,
c.postcode AS clientPostCode,
c.city AS clientCity,
c.fi,
p.name AS clientProvince,
ct.country AS clientCountry,
s.name AS supplierName,
s.street AS supplierStreet,
sc.country AS supplierCountry,
s.postCode AS supplierPostCode,
s.city AS supplierCity,
sp.name AS supplierProvince
FROM client c
JOIN account.user u ON u.id = c.id
JOIN country ct ON ct.id = c.countryFk
LEFT JOIN mandate m ON m.clientFk = c.id
AND m.companyFk = ? AND m.finished IS NULL
LEFT JOIN supplier s ON s.id = m.companyFk
LEFT JOIN country sc ON sc.id = s.countryFk
LEFT JOIN province sp ON sp.id = s.provinceFk
LEFT JOIN province p ON p.id = c.provinceFk
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
ORDER BY m.created DESC LIMIT 1`, [companyFk, companyFk, clientFk]);
},
dated: () => {
return strftime('%d-%m-%Y', new Date());
},
toISOString: date => {
return strftime('%d-%m-%Y', date);
},
},
components: {
'report-header': require('../report-header'),
'report-footer': require('../report-footer'),
},
};

View File

@ -1,36 +0,0 @@
module.exports = {
messages: {
es: {
title: 'Autorisation pour débit',
Creditor: 'Tireur',
Deptor: 'Tiré',
Bank: 'Banque',
description: `Nous, soussignés, autorisons que tout effet émis par le tireur , susmentionné, et tiré sur notre Société,
soit automatiquement débité dans notre compte selon les suivants détails de domiciliation:`,
authorization: `Cette autorisation maintient sa validité jusqu'à à la réception de
nouvelles instructions.`,
supplier: {
name: 'Nom',
street: 'Adresse'
},
bank: {
name: 'Nom',
street: 'Adresse',
account: 'RIB',
bankCode: 'Code banque',
agencyCode: 'Code agence',
accountNumber: 'Numero de compte',
ribKey: 'Clé RIB'
},
client: {
name: 'Nom',
street: 'Adresse',
fi: 'Siren',
sign: 'Signature autorisée du tiré',
signDate: 'Lieu et date',
toCompleteByClient: 'À remplir par le débiteur',
},
order: 'Ord. domiciliación {0}',
},
},
};

View File

@ -29,11 +29,13 @@ client:
swift: Swift BIC swift: Swift BIC
accountNumber: Número de cuenta - IBAN accountNumber: Número de cuenta - IBAN
accountHolder: "(Titular/es de la cuenta de cargo)" accountHolder: "(Titular/es de la cuenta de cargo)"
accountNumberFormat: En España el IBAN consta de {0} posiciones comenzando siempre accountNumberFormat: En {0} el IBAN consta de {1} posiciones comenzando siempre por {2}
por ES
paymentType: Tipo de pago paymentType: Tipo de pago
recurrent: Recurrente recurrent: Recurrente
unique: Único unique: Único
signLocation: Fecha - Localidad signLocation: Fecha - Localidad
sign: Firma del deudor y sello sign: Firma del deudor y sello
order: Ord. domiciliación {0} order: Ord. domiciliación {0}
Francia: Francia
España: España
Portugal: Portugal

View File

@ -35,3 +35,4 @@ client:
order: Réf. mandat {0} order: Réf. mandat {0}
Francia: France Francia: France
España: Espagne España: Espagne
Portugal: Portugal

View File

@ -29,11 +29,13 @@ client:
swift: Swift BIC swift: Swift BIC
accountNumber: Número de Conta IBAN accountNumber: Número de Conta IBAN
accountHolder: "(Titular(es) da conta)" accountHolder: "(Titular(es) da conta)"
accountNumberFormat: Em Portugal o IBAN é composto por 25 dígitos e começa sempre accountNumberFormat: Em {0} o IBAN é composto pelo {1} dígitos e começa sempre pelo {2}
por PT
paymentType: Tipos de pagamento Pagamento paymentType: Tipos de pagamento Pagamento
recurrent: Recorrente recurrent: Recorrente
unique: Pagamento pontual unique: Pagamento pontual
signLocation: Data - Localidade signLocation: Data - Localidade
sign: Assinatura e carimbo do devedor sign: Assinatura e carimbo do devedor
order: Referência da ordem {0} order: Referência da ordem {0}
Francia: França
España: Espanha
Portugal: Portugal

View File

@ -153,7 +153,7 @@
</tr> </tr>
<tr> <tr>
<td>{{$t('client.signLocation')}}</td> <td>{{$t('client.signLocation')}}</td>
<th>{{dated}}, {{supplier.province}}</th> <th>{{dated}}, {{client.province}}</th>
</tr> </tr>
<tr> <tr>
<td>{{$t('client.sign')}}</td> <td>{{$t('client.sign')}}</td>