This commit is contained in:
Carlos Jimenez Ruiz 2019-11-14 14:22:22 +01:00
commit 8629b72d55
61 changed files with 598 additions and 875 deletions

View File

@ -28,8 +28,10 @@ module.exports = Self => {
const models = Self.app.models;
const accessToken = ctx.req.accessToken;
const sender = await models.Account.findById(accessToken.userId);
const recipient = to.replace('@', '');
return sendMessage(to, `@${sender.name}: ${message}`);
if (sender.name != recipient)
return sendMessage(to, `@${sender.name}: ${message}`);
};
async function sendMessage(name, message) {

View File

@ -1,11 +1,18 @@
const app = require('vn-loopback/server/server');
describe('chat sendMessage()', () => {
it('should return a response', async() => {
it('should return a "Fake notification sent" as response', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent');
});
it('should not return a response', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response).toBeUndefined();
});
});

View File

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

@ -0,0 +1,10 @@
USE `vn`;
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 1);
UPDATE `vn`.`country` SET `ibanLength` = '27' WHERE (`id` = 2);
UPDATE `vn`.`country` SET `ibanLength` = '22' WHERE (`id` = 3);
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 4);
UPDATE `vn`.`country` SET `ibanLength` = '18' WHERE (`id` = 5);
UPDATE `vn`.`country` SET `ibanLength` = '25' WHERE (`id` = 8);
UPDATE `vn`.`country` SET `ibanLength` = '27' WHERE (`id` = 19);
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 30);

View File

@ -0,0 +1,8 @@
USE `vn`;
UPDATE `vn`.`sample` SET `description` = 'Bienvenida como nuevo cliente' WHERE (`id` = '12');
UPDATE `vn`.`sample` SET `description` = 'Instalación y configuración de impresora de coronas' WHERE (`id` = '13');
UPDATE `vn`.`sample` SET `description` = 'Solicitud de domiciliación bancaria' WHERE (`id` = '14');
UPDATE `vn`.`sample` SET `description` = 'Aviso inicial por saldo deudor' WHERE (`id` = '15');
UPDATE `vn`.`sample` SET `description` = 'Aviso reiterado por saldo deudor' WHERE (`id` = '16');
UPDATE `vn`.`sample` SET `isVisible` = '0' WHERE (`id` = '17');

View File

@ -0,0 +1,86 @@
USE `vn`;
ALTER TABLE `vn`.`ticketRequest`
DROP FOREIGN KEY `fgnAtender`;
ALTER TABLE `vn`.`ticketRequest`
CHANGE COLUMN `atenderFk` `attenderFk` INT(11) NULL DEFAULT NULL ;
ALTER TABLE `vn`.`ticketRequest`
ADD CONSTRAINT `fgnAtender`
FOREIGN KEY (`attenderFk`)
REFERENCES `vn`.`worker` (`id`)
ON UPDATE CASCADE;
USE `vn2008`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `vn2008`.`Ordenes` AS
SELECT
`tr`.`id` AS `Id_ORDEN`,
`tr`.`description` AS `ORDEN`,
`tr`.`requesterFk` AS `requesterFk`,
`tr`.`attenderFk` AS `attenderFk`,
`tr`.`quantity` AS `CANTIDAD`,
`tr`.`itemFk` AS `Id_ARTICLE`,
`tr`.`price` AS `PRECIOMAX`,
`tr`.`isOk` AS `isOk`,
`tr`.`saleFk` AS `Id_Movimiento`,
`tr`.`ticketFk` AS `ticketFk`,
`tr`.`response` AS `COMENTARIO`,
`tr`.`created` AS `odbc_date`,
`tr`.`ordered` AS `datORDEN`,
`tr`.`shipped` AS `datTICKET`,
`tr`.`salesPersonCode` AS `CodVENDEDOR`,
`tr`.`buyerCode` AS `CodCOMPRADOR`,
`tr`.`price__` AS `PREU`,
`tr`.`clientFk` AS `Id_CLIENTE`,
`tr`.`ok__` AS `OK`,
`tr`.`total` AS `TOTAL`,
`tr`.`buyed` AS `datCOMPRA`,
`tr`.`ko__` AS `KO`
FROM
`vn`.`ticketRequest` `tr`;
USE `vn`;
DROP TRIGGER IF EXISTS `vn`.`ticketRequest_beforeInsert`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `vn`.`ticketRequest_beforeInsert` BEFORE INSERT ON `ticketRequest` FOR EACH ROW
BEGIN
IF NEW.ticketFk IS NULL THEN
SET NEW.ticketFk = (SELECT s.ticketFk FROM sale s WHERE s.id = NEW.saleFk);
END IF;
IF NEW.requesterFk IS NULL THEN
SET NEW.requesterFk = (SELECT w.id FROM worker w WHERE w.code = NEW.salesPersonCode);
END IF;
IF NEW.attenderFk IS NULL THEN
SET NEW.attenderFk = (SELECT w.id FROM worker w WHERE w.code = NEW.buyerCode);
END IF;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn`.`ticketRequest_beforeUpdate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `vn`.`ticketRequest_beforeUpdate` BEFORE UPDATE ON `ticketRequest` FOR EACH ROW
BEGIN
IF NEW.saleFk <> OLD.saleFk THEN
SET NEW.ticketFk = (SELECT s.ticketFk FROM sale s WHERE s.id = NEW.saleFk);
END IF;
IF NEW.salesPersonCode <> OLD.salesPersonCode THEN
SET NEW.requesterFk = (SELECT w.id FROM worker w WHERE w.code = NEW.salesPersonCode);
END IF;
IF NEW.buyerCode <> OLD.buyerCode THEN
SET NEW.attenderFk = (SELECT w.id FROM worker w WHERE w.code = NEW.buyerCode);
END IF;
END$$
DELIMITER ;

View File

@ -53,13 +53,14 @@ INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossF
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`)
VALUES
(1, 'España', 0, 'ES', 1, 22),
(2, 'Italia', 1, 'IT', 1, 25),
(3, 'Alemania', 1, 'DE', 1, 20),
(4, 'Rumania', 1, 'RO', 1, 22),
(5, 'Holanda', 1, 'NL', 1, 16),
(19,'Francia', 1, 'FR', 1, 25),
(30,'Canarias', 1, 'IC', 1, 22);
(1, 'España', 0, 'ES', 1, 24),
(2, 'Italia', 1, 'IT', 1, 27),
(3, 'Alemania', 1, 'DE', 1, 22),
(4, 'Rumania', 1, 'RO', 1, 24),
(5, 'Holanda', 1, 'NL', 1, 18),
(8, 'Portugal', 1, 'PT', 1, 27),
(19,'Francia', 1, 'FR', 1, 27),
(30,'Canarias', 1, 'IC', 1, 24);
INSERT INTO `vn`.`warehouse`(`id`, `name`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`)
VALUES
@ -195,9 +196,9 @@ INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city
VALUES
(101, 'Bruce Wayne', '84612325V', 'Batman', 'Alfred', '1007 Mountain Drive, Gotham', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceWayne@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(102, 'Petter Parker', '87945234L', 'Spider man', 'Aunt May', '20 Ingram Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 19, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(104, 'Tony Stark', '06089160W', 'Iron man', 'Pepper Potts', '10880 Malibu Point', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(105, 'Max Eisenhardt', '251628698', 'Magneto', 'Rogue', 'Unknown Whereabouts', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1),
(105, 'Max Eisenhardt', '251628698', 'Magneto', 'Rogue', 'Unknown Whereabouts', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 8, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1),
(106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'Evil hideout', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1),
(107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(108, 'Charles Xavier', '22641921P', 'Professor X', 'Beast', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1),
@ -1471,7 +1472,7 @@ INSERT INTO `vn2008`.`workerTeam`(`id`, `team`, `user`)
(5, 3, 103),
(6, 3, 104);
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `atenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `attenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
VALUES
(1, 'Ranged weapon longbow 2m', 18, 35, 5, 1, 9.10, 1, 1, 1, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),
(2, 'Melee weapon combat first 15cm', 18, 35, 10, 2, 1.07, 0, NULL, 1, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),

View File

@ -5,25 +5,14 @@ describe('buyUltimateFromInterval()', () => {
let today;
let future;
beforeAll(() => {
let date = new Date();
let month = `${date.getMonth() + 1}`;
let futureMonth = `${date.getMonth() + 2}`;
let day = date.getDate();
let year = date.getFullYear();
let futureYear = year;
let now = new Date();
now.setHours(0, 0, 0, 0);
today = now;
if (month.toString().length < 2) month = '0' + month;
if (futureMonth.toString().length < 2) futureMonth = '0' + futureMonth;
if (futureMonth.toString() == '13') {
futureMonth = '01';
futureYear + 1;
}
if (day.toString().length < 2) day = `0${day}`;
today = [year, month, day].join('-');
future = [futureYear, futureMonth, day].join('-');
let futureDate = new Date(now);
let futureMonth = now.getMonth() + 1;
futureDate.setMonth(futureMonth);
future = futureDate;
});
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[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(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() => {
@ -101,8 +90,8 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(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() => {
@ -137,7 +126,7 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
});
});

View File

@ -111,9 +111,7 @@
"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 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 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}}})",
"You cannot remove this ticket because is already invoiced, deleted or prepared": "You cannot remove this ticket because is already invoiced, deleted or prepared"
"Has deleted the ticket id": "Ha eliminado el ticket id [#{{id}}]({{{url}}})"
}

View File

@ -15,7 +15,7 @@
<vn-treeview vn-id="treeview" root-label="Locations"
fetch-func="$ctrl.onFetch($item)"
sort-func="$ctrl.onSort($a, $b)">
<vn-check
<vn-check acl-role="deliveryBoss"
ng-model="item.selected"
on-change="$ctrl.onSelection(value, item)"
triple-state="true"

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

@ -76,7 +76,7 @@ module.exports = Self => {
case 'ticketFk':
return {'t.id': value};
case 'attenderFk':
return {'tr.atenderFk': value};
return {'tr.attenderFk': value};
case 'isOk':
return {'tr.isOk': value};
case 'clientFk':
@ -106,7 +106,7 @@ module.exports = Self => {
tr.ticketFk,
tr.quantity,
tr.price,
tr.atenderFk attenderFk,
tr.attenderFk,
tr.description,
tr.response,
tr.saleFk,
@ -131,7 +131,7 @@ module.exports = Self => {
LEFT JOIN sale s ON s.id = tr.saleFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.userFk
LEFT JOIN worker wka ON wka.id = tr.atenderFk
LEFT JOIN worker wka ON wka.id = tr.attenderFk
LEFT JOIN account.user ua ON ua.id = wka.userFk`);
stmt.merge(conn.makeSuffix(filter));

View File

@ -27,7 +27,7 @@ module.exports = Self => {
const $t = ctx.req.__; // $translate
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
const sales = await models.Sale.find({

View File

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

View File

@ -33,13 +33,6 @@
"isOk": {
"type": "Boolean"
},
"attenderFk": {
"type": "Number",
"required": true,
"mysql": {
"columnName": "atenderFk"
}
},
"response": {
"type": "String"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ class Controller extends Component {
showChangeShipped() {
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;
}
this.newShipped = this.ticket.shipped;

View File

@ -32,6 +32,11 @@ class Controller {
callback: this.createClaim,
show: () => this.isEditable
},
{
name: 'Recalculate price',
callback: this.calculateSalePrice,
show: () => this.hasOneSaleSelected()
},
];
this._sales = [];
this.imagesPath = '//verdnatura.es/vn-image-data/catalog';
@ -534,6 +539,21 @@ class Controller {
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'];

View File

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

View File

@ -1,5 +1,6 @@
import '../index.js';
import watcher from 'core/mocks/watcher';
import crudModel from 'core/mocks/crud-model';
describe('Ticket', () => {
describe('Component vnTicketSale', () => {
@ -40,6 +41,7 @@ describe('Ticket', () => {
$scope.watcher = watcher;
$scope.sms = {open: () => {}};
$scope.ticket = ticket;
$scope.model = crudModel;
$httpBackend = _$httpBackend_;
Object.defineProperties($state.params, {
id: {
@ -334,5 +336,27 @@ describe('Ticket', () => {
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

@ -30,8 +30,11 @@
"isConfirmed": {
"type": "Boolean"
},
"isRaid": {
"type": "Boolean"
"isVirtual": {
"type": "Boolean",
"mysql": {
"columnName": "isRaid"
}
},
"commission": {
"type": "Number"

View File

@ -0,0 +1,45 @@
// const app = require('vn-loopback/server/server');
describe('workerTimeControl addTimeEntry()', () => {
it('should return 1 result filtering by id', async() => {
// let ctx = {req: {accessToken: {userId: 106}}, args: {workerFk: 106}};
// const firstHour = new Date();
// firstHour.setHours(7, 0, 0, 0);
// const lastHour = new Date();
// lastHour.setDate(lastHour.getDate() + 1);
// lastHour.setHours(15, 0, 0, 0);
// const filter = {
// where: {
// timed: {between: [firstHour, lastHour]}
// }
// };
// let result = await app.models.WorkerTimeControl.filter(ctx, filter);
// expect(result.length).toEqual(4);
// });
// it('should return a privilege error for a non subordinate worker', async() => {
// let ctx = {req: {accessToken: {userId: 107}}, args: {workerFk: 106}};
// const firstHour = new Date();
// firstHour.setHours(7, 0, 0, 0);
// const lastHour = new Date();
// lastHour.setDate(lastHour.getDate() + 1);
// lastHour.setHours(15, 0, 0, 0);
// const filter = {
// where: {
// timed: {between: [firstHour, lastHour]}
// }
// };
// let error;
// await app.models.WorkerTimeControl.filter(ctx, filter).catch(e => {
// error = e;
// }).finally(() => {
// expect(error.message).toEqual(`You don't have enough privileges`);
// });
// expect(error).toBeDefined();
});
});

View File

@ -0,0 +1,45 @@
// const app = require('vn-loopback/server/server');
// describe('workerTimeControl filter()', () => {
// it('should return 1 result filtering by id', async() => {
// let ctx = {req: {accessToken: {userId: 106}}, args: {workerFk: 106}};
// const firstHour = new Date();
// firstHour.setHours(7, 0, 0, 0);
// const lastHour = new Date();
// lastHour.setDate(lastHour.getDate() + 1);
// lastHour.setHours(15, 0, 0, 0);
// const filter = {
// where: {
// timed: {between: [firstHour, lastHour]}
// }
// };
// let result = await app.models.WorkerTimeControl.filter(ctx, filter);
// expect(result.length).toEqual(4);
// });
// it('should return a privilege error for a non subordinate worker', async() => {
// let ctx = {req: {accessToken: {userId: 107}}, args: {workerFk: 106}};
// const firstHour = new Date();
// firstHour.setHours(7, 0, 0, 0);
// const lastHour = new Date();
// lastHour.setDate(lastHour.getDate() + 1);
// lastHour.setHours(15, 0, 0, 0);
// const filter = {
// where: {
// timed: {between: [firstHour, lastHour]}
// }
// };
// let error;
// await app.models.WorkerTimeControl.filter(ctx, filter).catch(e => {
// error = e;
// }).finally(() => {
// expect(error.message).toEqual(`You don't have enough privileges`);
// });
// expect(error).toBeDefined();
// });
// });

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

@ -2,7 +2,6 @@ module.exports = Self => {
require('../methods/worker/filter')(Self);
require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self);
require('../methods/worker/getWorkerInfo')(Self);
require('../methods/worker/getWorkedHours')(Self);
require('../methods/worker/sendMessage')(Self);
require('../methods/worker/getWorkerInfo')(Self);
};

View File

@ -24,55 +24,66 @@ describe('Worker', () => {
describe('hours() setter', () => {
it(`should set hours data at it's corresponding week day`, () => {
let wednesday = new Date(controller.started.getTime());
wednesday.setDate(wednesday.getDate() + 2);
let today = new Date();
$httpBackend.whenRoute('GET', 'WorkerTimeControls/filter')
.respond([
{
id: 1,
timed: controller.started.toJSON(),
userFk: 1
}, {
id: 2,
timed: wednesday.toJSON(),
userFk: 1
}, {
id: 3,
timed: wednesday.toJSON(),
userFk: 1
}
]);
controller.date = today;
$httpBackend.flush();
let hours = [
{
id: 1,
timed: controller.started.toJSON(),
userFk: 1
}, {
id: 2,
timed: today.toJSON(),
userFk: 1
}, {
id: 3,
timed: today.toJSON(),
userFk: 1
}
];
controller.hours = hours;
let todayInWeek = today.getDay() - 1;
expect(controller.weekDays.length).toEqual(7);
expect(controller.weekDays[0].hours.length).toEqual(1);
expect(controller.weekDays[2].hours.length).toEqual(2);
expect(controller.weekDays[todayInWeek].hours.length).toEqual(2);
});
});
describe('getWeekdayTotalHours() ', () => {
it(`should return a total worked hours from 07:00 to 15:00`, () => {
const hourOne = new Date();
hourOne.setHours(7, 0, 0, 0);
const hourTwo = new Date();
hourTwo.setHours(10, 0, 0, 0);
const hourThree = new Date();
hourThree.setHours(10, 20, 0, 0);
const hourFour = new Date();
hourFour.setHours(15, 0, 0, 0);
describe('getWorkedHours() ', () => {
fit(`should set the week days and the worked hours in today`, () => {
let today = new Date();
const weekday = {hours: [
{id: 1, timed: hourOne},
{id: 2, timed: hourTwo},
{id: 3, timed: hourThree},
{id: 4, timed: hourFour}
]};
$httpBackend.whenRoute('GET', 'WorkerTimeControls/filter').respond({});
$httpBackend.whenRoute('GET', 'WorkerCalendars/absences').respond({});
$httpBackend.whenRoute('GET', 'Workers/:id/getWorkedHours')
.respond([
{dated: today},
]);
const result = controller.getWeekdayTotalHours(weekday);
today.setHours(0, 0, 0, 0);
expect(result).toEqual('08:00');
let weekOffset = today.getDay() - 1;
if (weekOffset < 0) weekOffset = 6;
let started = new Date(today.getTime());
started.setDate(started.getDate() - weekOffset);
controller.started = started;
let ended = new Date(started.getTime());
ended.setHours(23, 59, 59, 59);
ended.setDate(ended.getDate() + 6);
controller.ended = ended;
controller.getWorkedHours(controller.started, controller.ended);
$httpBackend.flush();
expect(controller.started).toEqual(started);
});
});

View File

@ -23,12 +23,11 @@ body {
.grid-block {
min-width: 300px;
max-width: 600px;
width: 600px;
margin: 0 auto
margin: 0 auto;
color: #333
}
h1 {
font-weight: 100;
font-size: 1.5em;
color: #333;
font-size: 1.5em
}

View File

@ -8,7 +8,7 @@
"i18n": {
"locale": "es",
"fallbackLocale": "es",
"silentTranslationWarn": true
"silentTranslationWarn": false
},
"pdf": {
"format": "A4",

View File

@ -8,7 +8,7 @@ header .logo img {
header .topbar {
background-color: #95d831;
height: 25px
height: 10px
}
.topbar:after {

View File

@ -4,6 +4,6 @@
<section v-if="centerText" class="uppercase">{{centerText}}</section>
<section class="number">{{$t('numPages')}}</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>
</footer>

View File

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

View File

@ -10,8 +10,11 @@ let configFiles = [
];
for (let configFile of configFiles) {
if (fs.existsSync(configFile))
Object.assign(config, require(configFile));
if (fs.existsSync(configFile)) {
const conf = require(configFile);
for (let prop in conf)
Object.assign(config[prop], conf[prop]);
}
}
config.env = env;

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-block white vn-pa-lg">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dearClient')}},</p>
<p v-html="$t('clientData')"></p>
<p>{{$t('dear')}},</p>
<p v-html="$t('description', [ticketId])"></p>
<p v-html="$t('poll')"></p>
<p v-html="$t('help')"></p>
<p v-html="$t('conclusion')"></p>
</div>
</div>
<!-- Footer block -->

View File

@ -1,6 +1,10 @@
subject: Aquí tienes tu albarán
title: "¡Este es tu albarán!"
dearClient: Estimado cliente
clientData: A continuación adjuntamos tu albarán.
dear: Estimado cliente
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
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
description:
dear: Estimado cliente
instructions: Para poder tramitar tu solicitud de cambio de tu forma de pago a giro
bancario, te adjuntamos los documentos correspondientes a la ley de pago, que
tienes que cumplimentar y enviarnos.
conclusion: Gracias por tu atención.
instructions: <p>Dadas las excelentes relaciones existentes entre nuestras
dos empresas y para facilitar los procesos de pago de nuestras facturas,
sugerimos el uso del sistema de domiciliación bancaria SEPA CORE.</p>
<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

@ -0,0 +1,27 @@
subject: Autorisation pour débit
title: Autorisation pour débit
description:
dear: Messieurs
instructions: <p>É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.</p>
<p>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.</p>
<p>Dans le cas où 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.</p>
<p>Ce système étant basé sur la transmission de données de
manière électronique, le maniement de documents
physiques á été éliminé</p>
<p>En vous remercieront pour votre collaboration, nous vous
prions dagréer, Messieurs, nos salutations distinguées.</p>
conclusion: Bien cordialement

View File

@ -28,11 +28,11 @@
<div class="grid-block white vn-pa-lg">
<h1>{{ $t('title') }}</h1>
<p>{{$t('description.dear')}},</p>
<p>{{$t('description.instructions')}}</p>
<div v-html="$t('description.instructions')"></div>
<p>{{$t('description.conclusion')}}</p>
</div>
</div>
<!-- Block -->
<!-- Attachments block -->
<div class="grid-row" v-if="isPreview">
<div class="grid-block white vn-pa-lg">
<attachment v-for="attachment in attachments"

View File

@ -238,6 +238,7 @@
<div class="grid-row">
<div class="grid-block">
<report-footer id="pageFooter"
v-bind:show-phytosanitary="true"
v-bind:left-text="$t('ticket', [ticket.id])"
v-bind:center-text="client.socialName"
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
accountNumber: Número de cuenta - IBAN
accountHolder: "(Titular/es de la cuenta de cargo)"
accountNumberFormat: En España el IBAN consta de 24 posiciones comenzando siempre
por ES
accountNumberFormat: En {0} el IBAN consta de {1} posiciones comenzando siempre por {2}
paymentType: Tipo de pago
recurrent: Recurrente
unique: Único
signLocation: Fecha - Localidad
sign: Firma del deudor y sello
order: Ord. domiciliación {0}
Francia: Francia
España: España
Portugal: Portugal

View File

@ -1,39 +1,38 @@
title: Orden de domiciliación de adeudo SEPA CORE
description: Mediante la firma de esta orden de domiciliación, el deudor autoriza
(A) al acreedor a enviar instrucciones a la entidad del deudor para adeudar su cuenta
y (B) a la entidad para efectuar los adeudos en su cuenta siguiendo las instrucciones
del acreedor.Como parte de sus derechos, el deudor está legitimado al reembolso
por su entidad en los términos y condiciones del contrato suscrito con la misma.
La solicitud de reembolso deberá efectuarse dentro de las ocho semanas que adeudo
en cuenta. Puede obtener información adicional sobre sus derechos en su entidad
financiera.
documentCopy: Debe llevar a su Entidad Bancaria una copia del documento firmado para
que lo registre y evitar la devolución.
mandatoryFields: TODOS LOS CAMPOS HAN DE SER CUMPLIMENTADOS OBLIGATORIAMENTE.
sendOrder: UNA VEZ FIRMADA ESTA ORDEN DE DOMICILIACIÓN DEBE SER ENVIADA AL ACREEDOR
PARA SU CUSTODIA Y ES RECOMENDABLE FACILITAR UNA COPIA A SU ENTIDAD BANCARIA.
title: Mandat de domiciliation Européene LCR
description: En signant ce formulaire de mandat, vous autorisez VERDNATURA LEVANTE SL
à envoyer des instructions à votre banque pour débiter votre compte, et (B) votre banque
à débiter votre compte conformément aux instructions de VERDNATURA LEVANTE SL.
Vous bénéficiez dun droit au remboursement par votre banque selon les conditions décrites
dans la convention que vous avez passée avec elle. Toute demande de remboursement doit être
présentée dans les 8 semaines suivant la date de débit de votre compte.
Votre banque peut vous renseigner au sujet de vos droits relatifs à ce mandat.
documentCopy: Veuillez dater, signer et retourner ce document à votre banque.
mandatoryFields: TOUS LES CHAMPS DOIVENT ÊTRE REINSEGNÉS IMPÉRATIVEMENT.
sendOrder: APRÈS SIGNATURA, RENVOYER AU CRÉANCIER ET AU VOTRE ÉTABLISSEMENT FINANCIER.
supplier:
toCompleteBySupplier: A cumplimentar por el acreedor
orderReference: Referencia de la orden de domiciliación
identifier: Identificador del acreedor
name: Nombre del acreedor
street: Dirección
location: CP - Población - Provincia
country: País
toCompleteBySupplier: Á compléter pour le créancier
orderReference: Numéro de référence du mandat
identifier: Identifiant créancier
name: Nom du céancier
street: Adresse
location: CP - Commune - Départament
country: Pays
client:
toCompleteByClient: A cumplimentar por el deudor
name: Nombre del deudor/es
street: Dirección del deudor
location: CP - Población - Provincia
country: País del deudor
toCompleteByClient: Á compléter pour le débiteur
name: Nom du débiteur(s)
street: Adresse du(des) débiteur(s)
location: CP - Commune - Départament
country: País du(des) débiteur(s)
swift: Swift BIC
accountNumber: Número de cuenta - IBAN
accountHolder: "(Titular/es de la cuenta de cargo)"
accountNumberFormat: En España el IBAN consta de 24 posiciones comenzando siempre
por ES
paymentType: Tipo de pago
recurrent: Recurrente
unique: Único
signLocation: Fecha - Localidad
sign: Firma del deudor y sello
order: Ord. domiciliación {0}
accountNumber: Numéro de compte - IBAN
accountHolder: (Débiteur(s) de compte)
accountNumberFormat: En {0} l'IBAN compte {1} postes commençant toujours par {2}
paymentType: Type de paiemen
recurrent: Versement périodique
unique: Paiement unique
signLocation: Date - Commune
sign: Signature du débiteur et tampon
order: Réf. mandat {0}
Francia: France
España: Espagne
Portugal: Portugal

View File

@ -22,18 +22,20 @@ supplier:
country: País
client:
toCompleteByClient: A preencher pelo devedor
name: Nome do devedor (Titular da conta)
name: Nome do devedor
street: Dirección del deudor
location: Cod. Postal - Município - Distrito
country: País do devedor
swift: Swift BIC
accountNumber: Número de Conta IBAN
accountHolder: "(Titular/es de la cuenta de cargo)"
accountNumberFormat: Em Portugal o IBAN é composto por 25 dígitos e começa sempre
por PT
accountHolder: "(Titular(es) da conta)"
accountNumberFormat: Em {0} o IBAN é composto pelo {1} dígitos e começa sempre pelo {2}
paymentType: Tipos de pagamento Pagamento
recurrent: Recorrente
unique: Pagamento pontual
signLocation: Data - Localidade
sign: Assinatura e carimbo do devedor
order: Referência da ordem {0}
Francia: França
España: Espanha
Portugal: Portugal

View File

@ -32,7 +32,10 @@
</tr>
<tr>
<td>{{$t('supplier.identifier')}}</td>
<th>ES89000B97367486</th>
<th>
<div>ES89000B97367486</div>
<div>B97367486-000</div>
</th>
</tr>
<tr>
<td>{{$t('supplier.name')}}</td>
@ -110,14 +113,19 @@
<div class="field square">
<span>{{client.countryCode.substr(0, 1)}}</span>
<span>{{client.countryCode.substr(1, 1)}}</span>
<span v-for="i in client.ibanLength"></span>
<span v-for="i in (client.ibanLength - 2)"></span>
</div>
</td>
</tr>
<tr>
<td class="description" colspan="2">
<div class="line">
<span>{{$t('client.accountNumberFormat')}}</span>
<span>{{$t('client.accountNumberFormat', [
$t(`${client.country}`),
client.ibanLength,
client.countryCode
])}}
</span>
</div>
</td>
</tr>
@ -145,7 +153,7 @@
</tr>
<tr>
<td>{{$t('client.signLocation')}}</td>
<th>{{dated}}, {{supplier.province}}</th>
<th>{{dated}}, {{client.province}}</th>
</tr>
<tr>
<td>{{$t('client.sign')}}</td>