Merge branch 'dev' into 2995-supplierAccount-modificaciones

This commit is contained in:
Javi Gallego 2021-09-21 14:02:07 +02:00
commit 3365eadf5e
49 changed files with 842 additions and 469 deletions

View File

@ -6,6 +6,11 @@ ALTER TABLE `vn`.`address` AUTO_INCREMENT = 1;
ALTER TABLE `vn`.`zoneGeo` AUTO_INCREMENT = 1; ALTER TABLE `vn`.`zoneGeo` AUTO_INCREMENT = 1;
ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1; ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1;
INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES
('TOTALLY_SECURE_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`) INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
VALUES VALUES
('1', '6'); ('1', '6');
@ -125,14 +130,14 @@ INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
(1, 'Main Warehouse'), (1, 'Main Warehouse'),
(2, 'Silla'); (2, 'Silla');
INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`, `hasComission`, `aliasFk`, `countryFk`) INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`, `hasComission`, `aliasFk`, `countryFk`, `hasProduction`)
VALUES VALUES
(1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1), (1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1, 1),
(2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13), (2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13, 1),
(3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), (3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1, 1),
(4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), (4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1, 1),
(5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), (5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1, 1),
(13, 'Inventory', NULL, 1, 1, 1, 0, 0, 0, 0, 2, 1); (13, 'Inventory', NULL, 1, 1, 1, 0, 0, 0, 0, 2, 1, 0);
INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPreparedByPacking`, `code`, `pickingPlacement`, `path`) INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPreparedByPacking`, `code`, `pickingPlacement`, `path`)
VALUES VALUES
@ -208,7 +213,7 @@ UPDATE `vn`.`agencyMode` SET `deliveryMethodFk` = 1 WHERE `id` = 8;
UPDATE `vn`.`agencyMode` SET `deliveryMethodFk` = 4 WHERE `id` = 23; UPDATE `vn`.`agencyMode` SET `deliveryMethodFk` = 4 WHERE `id` = 23;
UPDATE `vn`.`agencyMode` SET `deliveryMethodFk` = 1 WHERE `id` = 10; UPDATE `vn`.`agencyMode` SET `deliveryMethodFk` = 1 WHERE `id` = 10;
UPDATE `vn`.`agencyMode` SET `web` = 1; UPDATE `vn`.`agencyMode` SET `web` = 1, `reportMail` = 'no-reply@gothamcity.com';
UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23; UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23;
@ -846,7 +851,7 @@ INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `isBox`, `create
(7, 2, 4, 71, DATE_ADD(CURDATE(), INTERVAL -3 MONTH), NULL, 1, 1, 18, NULL, 94), (7, 2, 4, 71, DATE_ADD(CURDATE(), INTERVAL -3 MONTH), NULL, 1, 1, 18, NULL, 94),
(8, 3, 5, 71, DATE_ADD(CURDATE(), INTERVAL -4 MONTH), NULL, 1, 1, 18, NULL, 94), (8, 3, 5, 71, DATE_ADD(CURDATE(), INTERVAL -4 MONTH), NULL, 1, 1, 18, NULL, 94),
(9, 3, 6, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), NULL, 1, 1, 18, NULL, 94), (9, 3, 6, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), NULL, 1, 1, 18, NULL, 94),
(10, 7, 7, 71, CURDATE(), NULL, 1, 1, 18, NULL, 94); (10, 7, 7, 71, NOW(), NULL, 1, 1, 18, NULL, 94);
INSERT INTO `vn`.`ticketPackaging`(`id`, `ticketFk`, `packagingFk`, `quantity`, `created`, `pvp`) INSERT INTO `vn`.`ticketPackaging`(`id`, `ticketFk`, `packagingFk`, `quantity`, `created`, `pvp`)
VALUES VALUES
@ -2410,3 +2415,5 @@ INSERT INTO `vn`.`expeditionScan` (`id`, `expeditionFk`, `scanned`, `palletFk`)
(8, 8, CURDATE(), 1), (8, 8, CURDATE(), 1),
(9, 9, CURDATE(), 1), (9, 9, CURDATE(), 1),
(10, 10, CURDATE(), 1); (10, 10, CURDATE(), 1);
CALL `cache`.`last_buy_refresh`(FALSE);

View File

@ -3,22 +3,18 @@ const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
describe('last_buy_refresh()', () => { describe('last_buy_refresh()', () => {
it(`should store some data on cache.last_buy`, async() => { it(`should store some data on cache.last_buy`, async() => {
let stmts = []; const stmts = [];
let stmt;
stmts.push('START TRANSACTION'); stmts.push('START TRANSACTION');
stmt = new ParameterizedSQL('CALL cache.last_buy_refresh(true)'); const lastBuyTableIndex = stmts.push(`SELECT * FROM cache.last_buy ORDER BY item_id ASC`) - 1;
stmts.push(stmt);
let lastBuyTableIndex = stmts.push(`SELECT * FROM cache.last_buy ORDER BY item_id ASC`) - 1;
stmts.push('ROLLBACK'); stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql); const result = await app.models.Ticket.rawStmt(sql);
let lastBuyTable = result[lastBuyTableIndex]; const lastBuyTable = result[lastBuyTableIndex];
expect(lastBuyTable.length).toEqual(12); expect(lastBuyTable.length).toEqual(12);

View File

@ -186,9 +186,10 @@ export default {
receivedB2BVNLCheckbox: 'vn-client-billing-data vn-check[label="Received B2B VNL"]', receivedB2BVNLCheckbox: 'vn-client-billing-data vn-check[label="Received B2B VNL"]',
swiftBic: 'vn-client-billing-data vn-autocomplete[ng-model="$ctrl.client.bankEntityFk"]', swiftBic: 'vn-client-billing-data vn-autocomplete[ng-model="$ctrl.client.bankEntityFk"]',
newBankEntityButton: 'vn-client-billing-data vn-icon-button[vn-tooltip="New bank entity"] > button', newBankEntityButton: 'vn-client-billing-data vn-icon-button[vn-tooltip="New bank entity"] > button',
newBankEntityName: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.name"]', newBankEntityName: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.data.name"]',
newBankEntityBIC: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.bic"]', newBankEntityBIC: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.data.bic"]',
newBankEntityCode: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.id"]', newBankEntityCountry: '.vn-dialog.shown vn-autocomplete[ng-model="$ctrl.data.countryFk"]',
newBankEntityCode: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.data.id"]',
acceptBankEntityButton: '.vn-dialog.shown button[response="accept"]', acceptBankEntityButton: '.vn-dialog.shown button[response="accept"]',
saveButton: 'vn-client-billing-data button[type=submit]', saveButton: 'vn-client-billing-data button[type=submit]',
watcher: 'vn-client-billing-data vn-watcher' watcher: 'vn-client-billing-data vn-watcher'

View File

@ -34,8 +34,9 @@ describe('Client Edit billing data path', () => {
it(`should create a new BIC code`, async() => { it(`should create a new BIC code`, async() => {
await page.waitToClick(selectors.clientBillingData.newBankEntityButton); await page.waitToClick(selectors.clientBillingData.newBankEntityButton);
await page.write(selectors.clientBillingData.newBankEntityName, 'Gotham City Bank'); await page.write(selectors.clientBillingData.newBankEntityName, 'Gotham City Bank');
await page.write(selectors.clientBillingData.newBankEntityCode, '9999');
await page.write(selectors.clientBillingData.newBankEntityBIC, 'GTHMCT'); await page.write(selectors.clientBillingData.newBankEntityBIC, 'GTHMCT');
await page.autocompleteSearch(selectors.clientBillingData.newBankEntityCountry, 'España');
await page.write(selectors.clientBillingData.newBankEntityCode, '9999');
await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton); await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton);
await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank'); await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank');
const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value'); const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value');

View File

@ -19,28 +19,42 @@
ng-if="$ctrl.displayControls"> ng-if="$ctrl.displayControls">
</vn-button> </vn-button>
</div> </div>
<div class="weekdays"> <div id="days-header" ng-class="{'hide-weeks': $ctrl.hideWeeks}">
<section <div class="week-numbers" ng-if="!$ctrl.hideWeeks"></div>
ng-repeat="day in ::$ctrl.weekDays" <div class="weekdays">
translate-attr="::{title: day.name}" <section
ng-click="$ctrl.selectWeekDay($event, day.index)"> ng-repeat="day in ::$ctrl.weekDays"
<span>{{::day.localeChar}}</span> translate-attr="::{title: day.name}"
</section> ng-click="$ctrl.selectWeekDay($event, day.index)">
<span>{{::day.localeChar}}</span>
</section>
</div>
</div> </div>
<div
class="days" <div id="days-container" ng-class="{'hide-weeks': $ctrl.hideWeeks}">
ng-class="{'hide-contiguous': $ctrl.hideContiguous}"> <div class="weeks" ng-if="!$ctrl.hideWeeks">
<section <section ng-repeat="week in $ctrl.weekNumbers"
ng-repeat="day in $ctrl.days" class="day">
class="day" <div class="day-number">
ng-class="::$ctrl.getDayClasses(day)" {{::week}}
vn-repeat-last </div>
on-last="$ctrl.repeatLast()"> </section>
<div </div>
class="day-number" <div
ng-click="$ctrl.select($event, day)"> class="days"
{{::day | date: 'd'}} ng-class="{'hide-contiguous': $ctrl.hideContiguous}">
</div> <section
</section> ng-repeat="day in $ctrl.days"
class="day"
ng-class="::$ctrl.getDayClasses(day)"
vn-repeat-last
on-last="$ctrl.repeatLast()">
<div
class="day-number"
ng-click="$ctrl.select($event, day)">
{{::day | date: 'd'}}
</div>
</section>
</div>
</div> </div>
</div> </div>

View File

@ -12,11 +12,12 @@ import './style.scss';
* @event move Emitted when month changes * @event move Emitted when month changes
*/ */
export default class Calendar extends FormInput { export default class Calendar extends FormInput {
constructor($element, $scope, vnWeekDays) { constructor($element, $scope, vnWeekDays, moment) {
super($element, $scope); super($element, $scope);
this.weekDays = vnWeekDays.locales; this.weekDays = vnWeekDays.locales;
this.defaultDate = new Date(); this.defaultDate = new Date();
this.displayControls = true; this.displayControls = true;
this.moment = moment;
} }
/** /**
@ -54,15 +55,23 @@ export default class Calendar extends FormInput {
); );
} }
lastDay() {
return new Date(
this.defaultDate.getFullYear(),
this.defaultDate.getMonth() + 1,
0
).getDate();
}
/** /**
* Repaints the calendar. * Repaints the calendar.
*/ */
repaint() { repaint() {
const firstWeekday = this.firstDay(this.defaultDate).getDay() - 1; const firstWeekday = this.firstDay(this.defaultDate).getDay() - 1;
let weekdayOffset = firstWeekday >= 0 ? firstWeekday : 6; this.weekdayOffset = firstWeekday >= 0 ? firstWeekday : 6;
let dayIndex = new Date(this.defaultDate.getTime()); let dayIndex = new Date(this.defaultDate.getTime());
dayIndex.setDate(1 - weekdayOffset); dayIndex.setDate(1 - this.weekdayOffset);
this.days = []; this.days = [];
@ -70,27 +79,55 @@ export default class Calendar extends FormInput {
this.days.push(new Date(dayIndex.getTime())); this.days.push(new Date(dayIndex.getTime()));
dayIndex.setDate(dayIndex.getDate() + 1); dayIndex.setDate(dayIndex.getDate() + 1);
} }
this.getWeekdays();
}
getWeekdays() {
if (!this.moment) return;
const totalSlots = this.lastDay() + this.weekdayOffset;
const weeks = Math.ceil(totalSlots / 7);
const dated = this.moment(this.defaultDate);
const firstWeekNumber = dated.set('date', 1).isoWeek();
const weekNumbers = [];
for (let w = 0; w < weeks; w++) {
let weekNumber = firstWeekNumber;
if (dated.get('month') == 0 && firstWeekNumber > 1 && w > 0)
weekNumber = 0;
weekNumbers.push(weekNumber + w);
}
this.weekNumbers = weekNumbers;
} }
/** /**
* Gets CSS classes to apply to the specified day. * Gets CSS classes to apply to the specified day.
* *
* @param {Date} day The day * @param {Date} date The date
* @return {Object} The CSS classes to apply * @return {Object} The CSS classes to apply
*/ */
getDayClasses(day) { getDayClasses(date) {
let wday = day.getDay(); let day = date.getDate();
let month = day.getMonth(); let wday = date.getDay();
let month = date.getMonth();
const currentDay = new Date().getDate();
const currentMonth = new Date().getMonth();
let classes = { let classes = {
today: day === currentDay && month === currentMonth,
weekend: wday === 6 || wday === 0, weekend: wday === 6 || wday === 0,
previous: month < this.month, previous: month < this.month,
current: month == this.month, current: month == this.month,
next: month > this.month, next: month > this.month,
event: this.hasEvents({$day: day}) event: this.hasEvents({$day: date})
}; };
let userClass = this.getClass({$day: day}); let userClass = this.getClass({$day: date});
if (userClass) classes[userClass] = true; if (userClass) classes[userClass] = true;
return classes; return classes;
@ -181,7 +218,7 @@ export default class Calendar extends FormInput {
} }
} }
} }
Calendar.$inject = ['$element', '$scope', 'vnWeekDays']; Calendar.$inject = ['$element', '$scope', 'vnWeekDays', 'moment'];
ngModule.vnComponent('vnCalendar', { ngModule.vnComponent('vnCalendar', {
template: require('./index.html'), template: require('./index.html'),
@ -193,6 +230,7 @@ ngModule.vnComponent('vnCalendar', {
formatDay: '&?', formatDay: '&?',
displayControls: '<?', displayControls: '<?',
hideYear: '<?', hideYear: '<?',
hideContiguous: '<?' hideContiguous: '<?',
hideWeeks: '<?'
} }
}); });

View File

@ -19,7 +19,14 @@
color: inherit; color: inherit;
} }
} }
& > .weekdays { & #days-header {
flex-direction: row;
display: flex
}
& #days-header > .week-numbers {
width: 10%
}
& #days-header > .weekdays {
display: flex; display: flex;
color: $color-font-secondary; color: $color-font-secondary;
margin-bottom: 8px; margin-bottom: 8px;
@ -27,17 +34,49 @@
font-weight: bold; font-weight: bold;
font-size: .8rem; font-size: .8rem;
text-align: center; text-align: center;
width: 90%;
& > section { & > section {
width: 14.28%; width: 14.28%;
cursor: pointer; cursor: pointer;
} }
} }
& > .days { & #days-header.hide-weeks {
& > .weekdays {
width: 100%
}
}
& > #days-container {
flex-direction: row;
display: flex
}
& > #days-container > .weeks {
display: flex; display: flex;
flex-direction: column;
color: $color-font-secondary;
font-weight: bold;
font-size: .8rem;
width: 10%;
& > .day {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
}
& #days-container.hide-weeks {
& > .days {
width: 100%
}
}
#days-container > .days {
display: flex;
flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
width: 90%;
& > .day { & > .day {
width: 14.28%; width: 14.28%;
@ -46,6 +85,17 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&.today {
color: $color-font-bg;
& > .day-number {
border: 2px solid $color-font-link;
&:hover {
background-color: lighten($color-font-link, 20%);
opacity: .8
}
}
}
&.weekend { &.weekend {
color: $color-font-secondary; color: $color-font-secondary;
} }

View File

@ -9,13 +9,15 @@ import 'angular-translate-loader-partial';
import '@uirouter/angularjs'; import '@uirouter/angularjs';
import 'mg-crud'; import 'mg-crud';
import 'oclazyload'; import 'oclazyload';
import 'angular-moment';
export const ngDeps = [ export const ngDeps = [
'ngAnimate', 'ngAnimate',
'pascalprecht.translate', 'pascalprecht.translate',
'ui.router', 'ui.router',
'mgCrud', 'mgCrud',
'oc.lazyLoad' 'oc.lazyLoad',
'angularMoment'
]; ];
import * as validator from 'validator'; import * as validator from 'validator';

188
front/package-lock.json generated
View File

@ -3,6 +3,181 @@
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"packages": {
"": {
"name": "salix-front",
"version": "1.0.0",
"license": "GPL-3.0",
"dependencies": {
"@uirouter/angularjs": "^1.0.20",
"angular": "^1.7.5",
"angular-animate": "^1.7.8",
"angular-moment": "^1.3.0",
"angular-translate": "^2.18.1",
"angular-translate-loader-partial": "^2.18.1",
"croppie": "^2.6.5",
"js-yaml": "^3.13.1",
"mg-crud": "^1.1.2",
"oclazyload": "^0.6.3",
"require-yaml": "0.0.1",
"validator": "^6.3.0"
}
},
"node_modules/@uirouter/angularjs": {
"version": "1.0.29",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.29.tgz",
"integrity": "sha512-RImWnBarNixkMto0o8stEaGwZmvhv5cnuOLXyMU2pY8MP2rgEF74ZNJTLeJCW14LR7XDUxVH8Mk8bPI6lxedmQ==",
"dependencies": {
"@uirouter/core": "6.0.7"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/@uirouter/core": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz",
"integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/angular": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.2.tgz",
"integrity": "sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw=="
},
"node_modules/angular-animate": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
},
"node_modules/angular-moment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"dependencies": {
"moment": ">=2.8.0 <3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/angular-translate": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
"integrity": "sha512-KohNrkH6J9PK+VW0L/nsRTcg5Fw70Ajwwe3Jbfm54Pf9u9Fd+wuingoKv+h45mKf38eT+Ouu51FPua8VmZNoCw==",
"dependencies": {
"angular": "^1.8.0"
},
"engines": {
"node": "*"
}
},
"node_modules/angular-translate-loader-partial": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.18.4.tgz",
"integrity": "sha512-bsjR+FbB0sdA2528E/ugwKdlPPQhA1looxLxI3otayBTFXBpED33besfSZhYAISLgNMSL038vSssfRUen9qD8w==",
"dependencies": {
"angular-translate": "~2.18.4"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/croppie": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/mg-crud": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha1-p6AWGzWSPK7/8ZpIBpS2V1vDggw=",
"dependencies": {
"angular": "^1.6.1"
}
},
"node_modules/moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"engines": {
"node": "*"
}
},
"node_modules/oclazyload": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha1-Kjirv/QJDAihEBZxkZRbWfLoJ5w="
},
"node_modules/require-yaml": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha1-LhsY2RPDuqcqWk03O28Tjd0sMr0=",
"dependencies": {
"js-yaml": "^4.0.0"
}
},
"node_modules/require-yaml/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/require-yaml/node_modules/js-yaml": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
"integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"node_modules/validator": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g=",
"engines": {
"node": ">= 0.10"
}
}
},
"dependencies": { "dependencies": {
"@uirouter/angularjs": { "@uirouter/angularjs": {
"version": "1.0.29", "version": "1.0.29",
@ -27,6 +202,14 @@
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz", "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA==" "integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
}, },
"angular-moment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": {
"moment": ">=2.8.0 <3.0.0"
}
},
"angular-translate": { "angular-translate": {
"version": "2.18.4", "version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz", "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
@ -78,6 +261,11 @@
"angular": "^1.6.1" "angular": "^1.6.1"
} }
}, },
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"oclazyload": { "oclazyload": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz", "resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",

View File

@ -12,6 +12,7 @@
"@uirouter/angularjs": "^1.0.20", "@uirouter/angularjs": "^1.0.20",
"angular": "^1.7.5", "angular": "^1.7.5",
"angular-animate": "^1.7.8", "angular-animate": "^1.7.8",
"angular-moment": "^1.3.0",
"angular-translate": "^2.18.1", "angular-translate": "^2.18.1",
"angular-translate-loader-partial": "^2.18.1", "angular-translate-loader-partial": "^2.18.1",
"croppie": "^2.6.5", "croppie": "^2.6.5",

View File

@ -1,40 +1,46 @@
<vn-dialog class="edit" <tpl-title translate>
vn-id="bankEntityDialog" New bank entity
on-open="$ctrl.resetData()" </tpl-title>
on-accept="$ctrl.onAccept()" <tpl-body>
message="New bank entity"> <p translate>Please, ensure you put the correct data!</p>
<tpl-body> <vn-horizontal>
<p translate>Please, ensure you put the correct data!</p> <vn-textfield
<vn-horizontal> vn-one
<vn-textfield vn-focus
vn-one vn-id="entityName"
vn-focus label="Name"
vn-id="entityName" ng-model="$ctrl.data.name"
label="Name" required="true">
ng-model="$ctrl.data.name" </vn-textfield>
required="true"> <vn-textfield
</vn-textfield> vn-one
</vn-horizontal> vn-focus
<vn-horizontal> vn-id="bic"
<vn-textfield label="Swift"
vn-one ng-model="$ctrl.data.bic"
vn-focus required="true">
vn-id="bic" </vn-textfield>
label="Swift" </vn-horizontal>
ng-model="$ctrl.data.bic" <vn-horizontal>
required="true"> <vn-autocomplete
</vn-textfield> vn-one
<vn-autocomplete vn-one vn-id="country"
ng-model="$ctrl.data.countryFk" ng-model="$ctrl.data.countryFk"
url="Countries" url="Countries"
show-field="country" fields="['id', 'country', 'code']"
value-field="id" show-field="country"
label="Country"> value-field="id"
</vn-autocomplete> label="Country">
</vn-horizontal> </vn-autocomplete>
</tpl-body> <vn-textfield
<tpl-buttons> vn-one
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/> ng-show="country.selection.code === 'ES'"
<button id= "saveBankEntity" response="accept" translate>Save</button> label="Entity code"
</tpl-buttons> ng-model="$ctrl.data.id">
</vn-dialog> </vn-textfield>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>

View File

@ -1,35 +1,24 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Component from 'core/lib/component'; import Dialog from 'core/components/dialog';
import './style.scss'; import './style.scss';
class Controller extends Component { class Controller extends Dialog {
open() { responseHandler(response) {
this.$.bankEntityDialog.show(); if (response !== 'accept')
} return super.responseHandler(response);
resetData() { if (!this.data.countryFk)
this.data = {}; throw new Error(`The country can't be empty`);
}
onAccept() { return this.$http.post(`bankEntities`, this.data)
try { .then(res => this.data.id = res.data.id)
if (!this.data.countryFk) .then(() => super.responseHandler(response))
throw new Error(`The country can't be empty`); .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
this.$http.post(`bankEntities`, this.data).then(res => {
this.vnApp.showMessage(this.$t('The bank entity has been created. You can save the data now'));
this.emit('response', {$response: res.data});
});
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
} }
} }
ngModule.vnComponent('vnNewBankEntity', { ngModule.vnComponent('vnNewBankEntity', {
template: require('./index.html'), slotTemplate: require('./index.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
data: '<', data: '<',

View File

@ -5,49 +5,35 @@ describe('Salix Component vnNewBankEntity', () => {
let $httpBackend; let $httpBackend;
let $scope; let $scope;
let $element; let $element;
let vnApp;
beforeEach(ngModule('salix')); beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_) => { beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
vnApp = _vnApp_;
jest.spyOn(vnApp, 'showError');
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$element = angular.element('<vn-dialog></dialog>'); $element = angular.element('<vn-dialog></vn-dialog>');
controller = $componentController('vnNewBankEntity', {$element, $scope}); const $transclude = {
$$boundTransclude: {
$$slots: []
}
};
controller = $componentController('vnNewBankEntity', {$element, $scope, $transclude});
controller.vnApp = {showSuccess: jest.fn()};
})); }));
describe('resetData()', () => { describe('responseHandler()', () => {
it('should reset the location in the controller', () => { it('should show a success message after the query to bankEntities', () => {
expect(controller.data).toBeUndefined();
controller.resetData();
expect(controller.data).toEqual({});
});
});
describe('onAccept()', () => {
it('should throw an error if there is no country id in the location', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.data = {};
controller.onAccept();
expect(controller.vnApp.showError).toHaveBeenCalledWith(`The country can't be empty`);
});
it('should do add the new bank entity', () => {
controller.data = { controller.data = {
countryFk: 1 countryFk: 1
}; };
$httpBackend.expectPOST('bankEntities', controller.data).respond(200, controller.data); $httpBackend.expectPOST('bankEntities', controller.data).respond({id: 1});
controller.onAccept(); controller.responseHandler('accept');
$httpBackend.flush(); $httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.data.id).toEqual(1);
}); });
}); });
}); });

View File

@ -10,3 +10,4 @@ The postcode has been created. You can save the data now: Se ha creado el códig
The city has been created: Se ha creado la ciudad The city has been created: Se ha creado la ciudad
The province has been created: Se ha creado la provincia The province has been created: Se ha creado la provincia
The bank entity has been created. You can save the data now: Se ha creado la entidad bancaria. Puedes guardar los datos ahora The bank entity has been created. You can save the data now: Se ha creado la entidad bancaria. Puedes guardar los datos ahora
Entity code: Código de la entidad

View File

@ -4,7 +4,7 @@
message="Edit photo"> message="Edit photo">
<tpl-body class="upload-photo"> <tpl-body class="upload-photo">
<vn-horizontal> <vn-horizontal>
<vn-one ng-if="file.value"> <vn-one ng-if="file.value || $ctrl.newPhoto.url">
<vn-horizontal> <vn-horizontal>
<vn-icon-button vn-none <vn-icon-button vn-none
icon="rotate_left" icon="rotate_left"
@ -20,12 +20,26 @@
</vn-horizontal> </vn-horizontal>
</vn-one> </vn-one>
<vn-one> <vn-one>
<vn-horizontal> <vn-vertical class="vn-mb-sm">
<vn-radio
label="Select from computer"
val="computer"
ng-model="$ctrl.uploadMethod"
tabindex="-1">
</vn-radio>
<vn-radio
label="Import from external URL"
val="URL"
ng-model="$ctrl.uploadMethod"
tabindex="-1">
</vn-radio>
</vn-vertical>
<vn-horizontal ng-if="$ctrl.uploadMethod == 'computer'">
<vn-input-file vn-id="file" <vn-input-file vn-id="file"
vn-one vn-one
label="File" label="File"
ng-model="$ctrl.newPhoto.files" ng-model="$ctrl.newPhoto.files"
on-change="$ctrl.updatePhotoPreview(value)" on-change="$ctrl.updatePhotoPreview(value[0])"
accept="{{$ctrl.allowedContentTypes}}" accept="{{$ctrl.allowedContentTypes}}"
required="true"> required="true">
<append> <append>
@ -37,6 +51,14 @@
</append> </append>
</vn-input-file> </vn-input-file>
</vn-horizontal> </vn-horizontal>
<vn-horizontal ng-if="$ctrl.uploadMethod == 'URL'">
<vn-textfield
vn-one
ng-model="$ctrl.newPhoto.url"
on-change="$ctrl.updatePhotoPreview(value)"
placeholder="https://">
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
label="Type" label="Type"

View File

@ -39,6 +39,7 @@ export default class UploadPhoto extends Component {
} }
]; ];
this.viewportType = 'normal'; this.viewportType = 'normal';
this.uploadMethod = 'computer';
this.getAllowedContentTypes(); this.getAllowedContentTypes();
} }
@ -64,11 +65,16 @@ export default class UploadPhoto extends Component {
set viewportSelection(value) { set viewportSelection(value) {
this._viewportSelection = value; this._viewportSelection = value;
if (value && this.newPhoto.files) { const hasFile = this.newPhoto.files || this.newPhoto.url;
this.displayEditor(); if (!value || !hasFile) return;
const files = this.newPhoto.files;
this.updatePhotoPreview(files); let file;
} if (this.uploadMethod == 'computer')
file = this.newPhoto.files[0];
else if (this.uploadMethod == 'URL')
file = this.newPhoto.url;
this.updatePhotoPreview(file);
} }
getAllowedContentTypes() { getAllowedContentTypes() {
@ -90,13 +96,15 @@ export default class UploadPhoto extends Component {
* @param {string} value * @param {string} value
*/ */
updatePhotoPreview(value) { updatePhotoPreview(value) {
if (value && value[0]) { if (value) {
if (!this.editor) this.displayEditor();
this.displayEditor();
const reader = new FileReader(); if (this.uploadMethod == 'computer') {
reader.onload = e => this.editor.bind({url: e.target.result}); const reader = new FileReader();
reader.readAsDataURL(value[0]); reader.onload = e => this.editor.bind({url: e.target.result});
reader.readAsDataURL(value);
} else if (this.uploadMethod == 'URL')
this.editor.bind({url: value});
} }
} }

View File

@ -24,17 +24,30 @@ describe('Salix', () => {
}); });
describe('viewportSelection()', () => { describe('viewportSelection()', () => {
it('should call to displayEditor() and updatePhotoPreview() methods', () => { it('should call to the updatePhotoPreview() method when uploadMethod property is set to "computer"', () => {
controller.displayEditor = jest.fn();
controller.updatePhotoPreview = jest.fn(); controller.updatePhotoPreview = jest.fn();
const files = [{name: 'test.jpg'}]; const files = [{name: 'test.jpg'}];
controller.newPhoto.files = files; controller.newPhoto.files = files;
controller.uploadMethod = 'computer';
controller.viewportSelection = {code: 'normal'}; controller.viewportSelection = {code: 'normal'};
expect(controller.displayEditor).toHaveBeenCalledWith(); const firstFile = files[0];
expect(controller.updatePhotoPreview).toHaveBeenCalledWith(files);
expect(controller.updatePhotoPreview).toHaveBeenCalledWith(firstFile);
});
it('should call to the updatePhotoPreview() method when uploadMethod property is set to "URL"', () => {
controller.updatePhotoPreview = jest.fn();
const url = 'http://gothamcity.com/batman.png';
controller.newPhoto.url = url;
controller.uploadMethod = 'URL';
controller.viewportSelection = {code: 'normal'};
expect(controller.updatePhotoPreview).toHaveBeenCalledWith(url);
}); });
}); });

View File

@ -4,3 +4,5 @@ File name: Nombre del fichero
Rotate left: Girar a la izquierda Rotate left: Girar a la izquierda
Rotate right: Girar a la derecha Rotate right: Girar a la derecha
Panoramic: Panorámico Panoramic: Panorámico
Select from computer: Seleccionar desde ordenador
Import from external URL: Importar desde URL externa

View File

@ -206,5 +206,8 @@
"A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero", "A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero",
"A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa", "A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa",
"Global invoicing failed": "[Facturación global] No se han podido facturar algunos clientes", "Global invoicing failed": "[Facturación global] No se han podido facturar algunos clientes",
"Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes" "Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes",
"Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio",
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente"
} }

View File

@ -217,19 +217,9 @@ module.exports = Self => {
if (isTaxDataCheckedChanged && !orgData.businessTypeFk) if (isTaxDataCheckedChanged && !orgData.businessTypeFk)
throw new UserError(`Can't verify data unless the client has a business type`); throw new UserError(`Can't verify data unless the client has a business type`);
} }
// Credit changes
if (changes.credit !== undefined) { if (changes.credit !== undefined)
await validateCreditChange(ctx, finalState); await Self.changeCredit(ctx, finalState, changes);
let filter = {fields: ['id'], where: {userFk: ctx.options.accessToken.userId}};
let worker = await Self.app.models.Worker.findOne(filter);
let newCredit = {
amount: changes.credit,
clientFk: finalState.id,
workerFk: worker ? worker.id : null
};
await Self.app.models.ClientCredit.create(newCredit);
}
}); });
Self.observe('after save', async ctx => { Self.observe('after save', async ctx => {
@ -332,42 +322,54 @@ module.exports = Self => {
await models.Chat.send(httpCtx, `@${currentWorker.user}`, message); await models.Chat.send(httpCtx, `@${currentWorker.user}`, message);
}; };
async function validateCreditChange(ctx, finalState) { // Credit change validations
let models = Self.app.models; Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
let userId = ctx.options.accessToken.userId; const models = Self.app.models;
const userId = ctx.options.accessToken.userId;
let currentUserIsManager = await models.Account.hasRole(userId, 'manager'); const isManager = await models.Account.hasRole(userId, 'manager', ctx.options);
if (currentUserIsManager) if (!isManager) {
return; const lastCredit = await models.ClientCredit.findOne({
where: {
clientFk: finalState.id
},
order: 'id DESC'
}, ctx.options);
let filter = { const lastAmount = lastCredit && lastCredit.amount;
fields: ['roleFk'], const lastWorkerId = lastCredit && lastCredit.workerFk;
where: { const lastWorkerIsManager = await models.Account.hasRole(lastWorkerId, 'manager', ctx.options);
maxAmount: {gt: ctx.data.credit}
}
};
let limits = await models.ClientCreditLimit.find(filter); if (lastAmount == 0 && lastWorkerIsManager)
throw new UserError(`You can't change the credit set to zero from a manager`);
if (limits.length == 0) const creditLimits = await models.ClientCreditLimit.find({
throw new UserError('Credit limits not found'); fields: ['roleFk'],
where: {
maxAmount: {gte: changes.credit}
}
}, ctx.options);
// Si el usuario no tiene alguno de los roles no continua const requiredRoles = [];
for (limit of creditLimits)
requiredRoles.push(limit.roleFk);
let requiredRoles = []; const userRequiredRoles = await models.RoleMapping.count({
for (limit of limits) roleId: {inq: requiredRoles},
requiredRoles.push(limit.roleFk); principalType: 'USER',
principalId: userId
}, ctx.options);
let where = { if (userRequiredRoles <= 0)
roleId: {inq: requiredRoles}, throw new UserError(`You don't have enough privileges to set this credit amount`);
principalType: 'USER', }
principalId: userId
};
let count = await models.RoleMapping.count(where);
if (count <= 0) await models.ClientCredit.create({
throw new UserError('The role cannot set this credit amount'); amount: changes.credit,
} clientFk: finalState.id,
workerFk: userId
}, ctx.options);
};
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
app.on('started', function() { app.on('started', function() {

View File

@ -1,4 +1,4 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('Client Model', () => { describe('Client Model', () => {
@ -14,8 +14,8 @@ describe('Client Model', () => {
} }
}; };
const ctx = {req: activeCtx}; const ctx = {req: activeCtx};
const chatModel = app.models.Chat; const chatModel = models.Chat;
const client = {id: 1101, name: 'Bruce Banner'}; const instance = {id: 1101, name: 'Bruce Banner'};
const previousWorkerId = 1106; // DavidCharlesHaller const previousWorkerId = 1106; // DavidCharlesHaller
const currentWorkerId = 1107; // HankPym const currentWorkerId = 1107; // HankPym
@ -29,7 +29,7 @@ describe('Client Model', () => {
it('should call to the Chat send() method for both workers', async() => { it('should call to the Chat send() method for both workers', async() => {
spyOn(chatModel, 'send').and.callThrough(); spyOn(chatModel, 'send').and.callThrough();
await app.models.Client.notifyAssignment(client, previousWorkerId, currentWorkerId); await models.Client.notifyAssignment(instance, previousWorkerId, currentWorkerId);
expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`); expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`);
expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`); expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`);
@ -38,7 +38,7 @@ describe('Client Model', () => {
it('should call to the Chat send() method for the previous worker', async() => { it('should call to the Chat send() method for the previous worker', async() => {
spyOn(chatModel, 'send').and.callThrough(); spyOn(chatModel, 'send').and.callThrough();
await app.models.Client.notifyAssignment(client, null, currentWorkerId); await models.Client.notifyAssignment(instance, null, currentWorkerId);
expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`); expect(chatModel.send).toHaveBeenCalledWith(ctx, '@HankPym', `Client assignment has changed`);
}); });
@ -46,9 +46,69 @@ describe('Client Model', () => {
it('should call to the Chat send() method for the current worker', async() => { it('should call to the Chat send() method for the current worker', async() => {
spyOn(chatModel, 'send').and.callThrough(); spyOn(chatModel, 'send').and.callThrough();
await app.models.Client.notifyAssignment(client, previousWorkerId, null); await models.Client.notifyAssignment(instance, previousWorkerId, null);
expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`); expect(chatModel.send).toHaveBeenCalledWith(ctx, '@DavidCharlesHaller', `Client assignment has changed`);
}); });
}); });
describe('changeCredit()', () => {
it('should fail to change the credit as a salesAssistant set to zero by a manager', async() => {
const tx = await models.Client.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const context = {options};
// Set credit to zero by a manager
const financialBoss = await models.Account.findOne({
where: {name: 'financialBoss'}
}, options);
context.options.accessToken = {userId: financialBoss.id};
await models.Client.changeCredit(context, instance, {credit: 0});
const salesAssistant = await models.Account.findOne({
where: {name: 'salesAssistant'}
}, options);
context.options.accessToken = {userId: salesAssistant.id};
await models.Client.changeCredit(context, instance, {credit: 300});
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`You can't change the credit set to zero from a manager`);
});
it('should fail to change to a high credit amount as a salesAssistant', async() => {
const tx = await models.Client.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const context = {options};
const salesAssistant = await models.Account.findOne({
where: {name: 'salesAssistant'}
}, options);
context.options.accessToken = {userId: salesAssistant.id};
await models.Client.changeCredit(context, instance, {credit: 99999});
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`You don't have enough privileges to set this credit amount`);
});
});
}); });

View File

@ -66,7 +66,7 @@
<vn-icon-button <vn-icon-button
vn-auto vn-auto
icon="add_circle" icon="add_circle"
ng-click="$ctrl.onAddEntityClick($event)" vn-click-stop="bankEntity.show({countryFk: $ctrl.client.countryFk})"
vn-tooltip="New bank entity" vn-tooltip="New bank entity"
vn-acl="salesAssistant"> vn-acl="salesAssistant">
</vn-icon-button> </vn-icon-button>
@ -108,53 +108,8 @@
</vn-button-bar> </vn-button-bar>
</form> </form>
<!-- Create bank entity dialog --> <!-- New bankentity dialog -->
<vn-dialog class="edit" <vn-new-bank-entity
vn-id="bankEntityDialog" vn-id="bankEntity"
on-accept="$ctrl.onBankEntityAccept()" on-accept="$ctrl.onAccept($data)">
message="New bank entity"> </vn-new-bank-entity>
<tpl-body>
<vn-horizontal>
<vn-textfield
vn-one
label="Name"
ng-model="$ctrl.newBankEntity.name"
required="true"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="country"
label="Country"
ng-model="$ctrl.newBankEntity.countryFk"
fields="['id', 'country', 'code']"
url="Countries"
value-field="id"
show-field="country"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Entity Code"
ng-model="$ctrl.newBankEntity.id"
ng-show="country.selection.code === 'ES'">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Swift / BIC"
ng-model="$ctrl.newBankEntity.bic"
required="true">
</vn-textfield>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Create</button>
</tpl-buttons>
</vn-dialog>

View File

@ -37,17 +37,8 @@ export default class Controller extends Section {
return payMethod || iban || dueDay; return payMethod || iban || dueDay;
} }
onAddEntityClick(event) { onAccept(data) {
event.preventDefault(); this.client.bankEntityFk = data.id;
this.newBankEntity = {
countryFk: Number.parseInt(this.client.countryFk)
};
this.$.bankEntityDialog.show();
}
onBankEntityAccept() {
return this.$http.post(`BankEntities`, this.newBankEntity)
.then(res => this.client.bankEntityFk = res.data.id);
} }
get ibanCountry() { get ibanCountry() {

View File

@ -35,20 +35,12 @@ describe('Client', () => {
}); });
}); });
describe('onBankEntityAccept()', () => { describe('onAccept()', () => {
it('should request to create a new bank entity', () => { it('should assign the response id to the client bankEntityFk', () => {
let newBankEntity = { const expectedResponse = {id: 999};
name: 'My new bank entity', controller.onAccept(expectedResponse);
bic: 'ES123',
countryFk: 1,
id: 999
};
controller.newBankEntity = newBankEntity;
$httpBackend.expectPOST('BankEntities', newBankEntity).respond({id: 999});
controller.onBankEntityAccept();
$httpBackend.flush();
expect(controller.client.bankEntityFk).toEqual(newBankEntity.id); expect(controller.client.bankEntityFk).toEqual(expectedResponse.id);
}); });
}); });

View File

@ -15,4 +15,3 @@ Received B2B VNL: Recibido B2B VNL
Save: Guardar Save: Guardar
New bank entity: Nueva entidad bancaria New bank entity: Nueva entidad bancaria
Name can't be empty: El nombre no puede quedar vacío Name can't be empty: El nombre no puede quedar vacío
Entity Code: Código

View File

@ -182,8 +182,7 @@
vn-one vn-one
label="Verified data" label="Verified data"
ng-model="$ctrl.client.isTaxDataChecked" ng-model="$ctrl.client.isTaxDataChecked"
vn-acl="salesAssistant" vn-acl="salesAssistant">
disabled="!$ctrl.client.businessTypeFk">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -5,7 +5,7 @@
filter="::$ctrl.ticketFilter" filter="::$ctrl.ticketFilter"
limit="5" limit="5"
data="tickets" data="tickets"
order="shipped DESC"> order="shipped DESC, id">
</vn-crud-model> </vn-crud-model>
<vn-card class="summary"> <vn-card class="summary">
<h5> <h5>
@ -314,7 +314,7 @@
{{::ticket.nickname}} {{::ticket.nickname}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink> <vn-td expand>
{{::ticket.agencyMode.name}} {{::ticket.agencyMode.name}}
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>

View File

@ -115,7 +115,6 @@ module.exports = Self => {
const stmts = []; const stmts = [];
let stmt; let stmt;
stmts.push('CALL cache.last_buy_refresh(FALSE)');
stmts.push('CALL cache.visible_refresh(@calc_id, FALSE, 1)'); stmts.push('CALL cache.visible_refresh(@calc_id, FALSE, 1)');
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`

View File

@ -57,6 +57,11 @@ module.exports = Self => {
arg: 'stemMultiplier', arg: 'stemMultiplier',
type: 'integer', type: 'integer',
description: 'The item multiplier', description: 'The item multiplier',
},
{
arg: 'landed',
type: 'date',
description: 'The item last buy landed date',
} }
], ],
returns: { returns: {
@ -114,6 +119,8 @@ module.exports = Self => {
return {'ori.code': value}; return {'ori.code': value};
case 'intrastat': case 'intrastat':
return {'intr.description': value}; return {'intr.description': value};
case 'landed':
return {'lb.landed': value};
} }
}); });
@ -146,7 +153,8 @@ module.exports = Self => {
ic.name AS category, ic.name AS category,
intr.description AS intrastat, intr.description AS intrastat,
b.grouping, b.grouping,
b.packing b.packing,
lb.landing AS landed
FROM item i FROM item i
LEFT JOIN itemType it ON it.id = i.typeFk LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk LEFT JOIN itemCategory ic ON ic.id = it.categoryFk

View File

@ -26,6 +26,7 @@
<vn-th field="density" shrink>Density</vn-th> <vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th> <vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th> <vn-th field="active" shrink>Active</vn-th>
<vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th></vn-th> <vn-th></vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
@ -87,6 +88,7 @@
ng-model="::item.isActive"> ng-model="::item.isActive">
</vn-check> </vn-check>
</vn-td> </vn-td>
<vn-td shrink-date>{{::item.landed | date:'dd/MM/yyyy'}}</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-horizontal class="buttons"> <vn-horizontal class="buttons">
<vn-icon-button <vn-icon-button

View File

@ -223,7 +223,8 @@ module.exports = Self => {
MINUTE(z.hour) AS zoneMinute, MINUTE(z.hour) AS zoneMinute,
z.name AS zoneName, z.name AS zoneName,
z.id AS zoneFk, z.id AS zoneFk,
CAST(z.hour AS CHAR) AS hour CAST(z.hour AS CHAR) AS hour,
TIME_FORMAT(zed.etc, '%H:%i') AS practicalHour
FROM ticket t FROM ticket t
LEFT JOIN invoiceOut io ON t.refFk = io.ref LEFT JOIN invoiceOut io ON t.refFk = io.ref
LEFT JOIN zone z ON z.id = t.zoneFk LEFT JOIN zone z ON z.id = t.zoneFk
@ -235,7 +236,8 @@ module.exports = Self => {
LEFT JOIN state st ON st.id = ts.stateFk LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN client c ON c.id = t.clientFk LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.userFk`); LEFT JOIN account.user u ON u.id = wk.userFk
LEFT JOIN zoneEstimatedDelivery zed ON zed.zoneFk = t.zoneFk`);
if (args.orderFk) { if (args.orderFk) {
stmt.merge({ stmt.merge({

View File

@ -7,3 +7,5 @@ All the selected elements will be deleted. Are you sure you want to continue?: T
Component lack: Faltan componentes Component lack: Faltan componentes
Minimize/Maximize: Minimizar/Maximizar Minimize/Maximize: Minimizar/Maximizar
Problems: Problemas Problems: Problemas
Theoretical: Teórica
Practical: Práctica

View File

@ -37,8 +37,9 @@
<vn-th field="nickname">Client</vn-th> <vn-th field="nickname">Client</vn-th>
<vn-th field="salesPersonFk" class="expendable" shrink>Salesperson</vn-th> <vn-th field="salesPersonFk" class="expendable" shrink>Salesperson</vn-th>
<vn-th field="shipped" shrink-date>Date</vn-th> <vn-th field="shipped" shrink-date>Date</vn-th>
<vn-th>Hour</vn-th> <vn-th>Prep.</vn-th>
<vn-th field="zoneHour" shrink>Closure</vn-th> <vn-th field="hour" shrink>Theoretical</vn-th>
<vn-th field="practicalHour">Practical</vn-th>
<vn-th field="provinceFk" class="expendable">Province</vn-th> <vn-th field="provinceFk" class="expendable">Province</vn-th>
<vn-th field="stateFk">State</vn-th> <vn-th field="stateFk">State</vn-th>
<vn-th field="zoneFk">Zone</vn-th> <vn-th field="zoneFk">Zone</vn-th>
@ -112,6 +113,7 @@
</vn-td> </vn-td>
<vn-td shrink>{{::ticket.shipped | date: 'HH:mm'}}</vn-td> <vn-td shrink>{{::ticket.shipped | date: 'HH:mm'}}</vn-td>
<vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td> <vn-td shrink>{{::ticket.zoneLanding | date: 'HH:mm'}}</vn-td>
<vn-td shrink>{{::ticket.practicalHour | date: 'HH:mm'}}</vn-td>
<vn-td class="expendable">{{::ticket.province}}</vn-td> <vn-td class="expendable">{{::ticket.province}}</vn-td>
<vn-td class="expendable"> <vn-td class="expendable">
<span <span

View File

@ -74,12 +74,8 @@ export default class Controller extends Section {
return {'t.shipped': { return {'t.shipped': {
between: this.dateRange(value)} between: this.dateRange(value)}
}; };
case 'id':
case 'refFk':
case 'zoneFk': case 'zoneFk':
case 'nickname': case 'nickname':
case 'agencyModeFk':
case 'warehouseFk':
return {[`t.${param}`]: value}; return {[`t.${param}`]: value};
} }
} }

View File

@ -41,6 +41,14 @@
label="Beneficiary" label="Beneficiary"
ng-model="supplierAccount.beneficiary" ng-model="supplierAccount.beneficiary"
info="Beneficiary information"> info="Beneficiary information">
<append>
<vn-icon-button
vn-auto
icon="add_circle"
vn-click-stop="bankEntity.show({index: $index})"
vn-tooltip="New bank entity">
</vn-icon-button>
</append>
</vn-textfield> </vn-textfield>
<vn-none> <vn-none>
<vn-icon-button <vn-icon-button
@ -67,10 +75,11 @@
</vn-submit> </vn-submit>
</vn-button-bar> </vn-button-bar>
</form> </form>
<!-- New bankentity dialog --> <!-- New bankentity dialog -->
<vn-new-bank-entity <vn-new-bank-entity
vn-id="bankEntity" vn-id="bankEntity"
on-response="$ctrl.onResponse($response)"> on-accept="$ctrl.onAccept($data)">
</vn-new-bank-entity> </vn-new-bank-entity>
<vn-confirm <vn-confirm

View File

@ -26,27 +26,10 @@ class Controller extends Section {
}); });
} }
onResponse(response) { onAccept(data) {
const data = this.$.model.data; const accounts = this.supplierAccounts;
const supplierAccount = data[this.currentRowIndex]; const targetAccount = accounts[data.index];
supplierAccount.bankEntityFk = response.id; targetAccount.bankEntityFk = data.id;
}
showBankEntity(event, $index) {
if (event.defaultPrevented) return;
event.preventDefault();
this.currentRowIndex = $index;
this.$.bankEntity.open();
}
setWireTransfer() {
const values = {
id: this.$params.id,
payMethodFk: this.wireTransferFk
};
const query = `Suppliers/${this.$params.id}`;
return this.$http.patch(query, values)
.then(() => this.$.watcher.notifySaved());
} }
onSubmit() { onSubmit() {

View File

@ -5,17 +5,9 @@ import crudModel from 'core/mocks/crud-model';
describe('Supplier Component vnSupplierAccount', () => { describe('Supplier Component vnSupplierAccount', () => {
let $scope; let $scope;
let controller; let controller;
let $httpBackend;
let $httpParamSerializer;
let vnApp;
beforeEach(ngModule('supplier')); beforeEach(ngModule('supplier'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_, _vnApp_) => { beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
vnApp = _vnApp_;
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
jest.spyOn(vnApp, 'showError');
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$scope.model = crudModel; $scope.model = crudModel;
$scope.watcher = watcher; $scope.watcher = watcher;
@ -34,58 +26,20 @@ describe('Supplier Component vnSupplierAccount', () => {
}; };
})); }));
describe('showBankEntity()', () => { describe('onAccept()', () => {
it('should do nothing if it default is prevented', () => { it('should set the created bank entity id into the target account', () => {
const event = { controller.supplierAccounts = [{}, {}, {}];
defaultPrevented: true,
preventDefault: () => {}
};
jest.spyOn(event, 'preventDefault');
jest.spyOn(controller.$.bankEntity, 'open');
controller.showBankEntity(event); const data = {
id: 999,
expect(event.preventDefault).not.toHaveBeenCalledWith(); index: 1
expect(controller.$.bankEntity.open).not.toHaveBeenCalledWith();
});
it('should call preventDefault() and open() when the default is not prevented', () => {
const event = {
defaultPrevented: false,
preventDefault: () => {}
}; };
jest.spyOn(event, 'preventDefault'); controller.onAccept(data);
jest.spyOn(controller.$.bankEntity, 'open');
controller.showBankEntity(event); const targetAccount = controller.supplierAccounts[data.index];
expect(event.preventDefault).toHaveBeenCalledWith(); expect(targetAccount.bankEntityFk).toEqual(data.id);
expect(controller.$.bankEntity.open).toHaveBeenCalledWith();
});
it('should set pay method to wireTransfer', () => {
controller.bankEntity = {
name: 'My new bank entity',
bic: 'ES1234',
countryFk: 1,
id: 2200
};
const expectedParams = {
filter: {
where: {code: 'wireTransfer'}
}
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.when('GET', `payMethods/findOne?${serializedParams}`).respond({id: 1});
const query = `SupplierAccounts/${controller.$.bankEntity.id}/createBankEntity`;
$httpBackend.expectPATCH(query).respond({id: 2200});
controller.onBankEntityAccept();
$httpBackend.flush();
expect(controller.supplierAccount.bankEntityFk).toEqual(controller.bankEntity.id);
}); });
}); });

View File

@ -2,7 +2,7 @@
vn-id="model" vn-id="model"
url="Tickets/filter" url="Tickets/filter"
limit="20" limit="20"
order="shippedDate DESC, shippedHour ASC, zoneLanding ASC"> order="shippedDate DESC, shippedHour ASC, zoneLanding ASC, id">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar

View File

@ -78,6 +78,11 @@
</vn-avatar> </vn-avatar>
<span translate>Festive</span> <span translate>Festive</span>
</vn-chip> </vn-chip>
<vn-chip>
<vn-avatar class="today">
</vn-avatar>
<span translate>Current day</span>
</vn-chip>
</div> </div>
</div> </div>
</vn-side-menu> </vn-side-menu>

View File

@ -8,3 +8,4 @@ days: días
Choose an absence type from the right menu: Elige un tipo de ausencia desde el menú de la derecha Choose an absence type from the right menu: Elige un tipo de ausencia desde el menú de la derecha
To start adding absences, click an absence type from the right menu and then on the day you want to add an absence: Para empezar a añadir ausencias, haz clic en un tipo de ausencia desde el menu de la derecha y después en el día que quieres añadir la ausencia To start adding absences, click an absence type from the right menu and then on the day you want to add an absence: Para empezar a añadir ausencias, haz clic en un tipo de ausencia desde el menu de la derecha y después en el día que quieres añadir la ausencia
You can just add absences within the current year: Solo puedes añadir ausencias dentro del año actual You can just add absences within the current year: Solo puedes añadir ausencias dentro del año actual
Current day: Día actual

View File

@ -42,11 +42,19 @@ vn-worker-calendar {
border-bottom: 1px solid rgba(0, 0, 0, 0.3); border-bottom: 1px solid rgba(0, 0, 0, 0.3);
} }
.festive { vn-avatar.festive,
background-color:white; vn-avatar.today {
border: 2px solid $color-alert; background-color: $color-font-dark;
width: 24px; width: 24px;
min-width: 24px; min-width: 24px;
height: 24px height: 24px
} }
vn-avatar.festive {
border: 2px solid $color-alert
}
vn-avatar.today {
border: 2px solid $color-font-link
}
} }

View File

@ -25,7 +25,7 @@ module.exports = {
throw err; throw err;
}).finally(async() => { }).finally(async() => {
await db.rawSql(` await db.rawSql(`
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status) INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, status)
VALUES (?, ?, 1, ?, ?, ?)`, [ VALUES (?, ?, 1, ?, ?, ?)`, [
options.to, options.to,
options.replyTo, options.replyTo,

View File

@ -143,8 +143,24 @@ module.exports = app => {
AND t.refFk IS NULL AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]); GROUP BY e.ticketFk`, [reqArgs.routeId]);
const ticketIds = tickets.map(ticket => ticket.id); const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs); await closeAll(ticketIds, reqArgs);
// Send route report to the agency
const agencyMail = await db.findValue(`
SELECT am.reportMail
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id = ?`, [reqArgs.routeId]);
if (agencyMail) {
const args = Object.assign({
routeId: reqArgs.routeId,
recipient: agencyMail
}, reqArgs);
const email = new Email('driver-route', args);
await email.send();
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@ -3,21 +3,3 @@ title: Orden de recogida
description: description:
dear: Estimado cliente dear: Estimado cliente
instructions: Aqui tienes tu orden de recogida. instructions: Aqui tienes tu orden de recogida.
sections:
howToBuy:
title: Cómo hacer un pedido
description: 'Para realizar un pedido en nuestra web, debes configurarlo indicando:'
requeriments:
- Si quieres recibir el pedido (por agencia o por nuestro propio reparto) o si
lo prefieres recoger en alguno de nuestros almacenes.
- La fecha en la que quieres recibir el pedido (se preparará el día anterior).
- La dirección de entrega o el almacén donde quieres recoger el pedido.
stock: En nuestra web y aplicaciones puedes visualizar el stock disponible de
flor cortada, verdes, plantas, complementos y artificial. Ten en cuenta que
dicho stock puede variar en función de la fecha seleccionada al configurar el
pedido. Es importante CONFIRMAR los pedidos para que la mercancía quede reservada.
delivery: El reparto se realiza de lunes a sábado según la zona en la que te encuentres.
Por regla general, los pedidos que se entregan por agencia, deben estar confirmados
y pagados antes de las 17h del día en que se preparan (el día anterior a recibirlos),
aunque esto puede variar si el pedido se envía a través de nuestro reparto y
según la zona.

View File

@ -118,20 +118,20 @@
</table> </table>
<!-- End of sales block --> <!-- End of sales block -->
<div class="columns"> <div class="columns vn-mb-ml">
<!-- Services block--> <!-- Services block-->
<div class="size100 no-page-break" v-if="services.length > 0"> <div class="size100 no-page-break" v-if="services.length > 0">
<h2>{{$t('services')}}</h2> <h2>{{$t('services.title')}}</h2>
<table class="column-oriented"> <table class="column-oriented">
<thead> <thead>
<tr> <tr>
<th width="5%"></th> <th width="5%"></th>
<th class="number">{{$t('quantity')}}</th> <th class="number">{{$t('services.theader.quantity')}}</th>
<th width="50%">{{$t('concept')}}</th> <th width="50%">{{$t('services.theader.concept')}}</th>
<th class="number">{{$t('price')}}</th> <th class="number">{{$t('services.theader.price')}}</th>
<th class="centered" width="5%"></th> <th class="centered" width="5%"></th>
<th class="centered">{{$t('vat')}}</th> <th class="centered">{{$t('services.theader.vat')}}</th>
<th class="number">{{$t('amount')}}</th> <th class="number">{{$t('services.theader.amount')}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -148,25 +148,26 @@
<tfoot> <tfoot>
<tr> <tr>
<td colspan="6" class="font bold"> <td colspan="6" class="font bold">
<span class="pull-right">{{$t('subtotal')}}</span> <span class="pull-right">{{$t('services.tfoot.subtotal')}}</span>
</td> </td>
<td class="number">{{serviceTotal | currency('EUR', $i18n.locale)}}</td> <td class="number">{{serviceTotal | currency('EUR', $i18n.locale)}}</td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
<span class="font gray">* {{ $t('services.warning') }}</span>
</div> </div>
<!-- End of services block --> <!-- End of services block -->
</div> </div>
<div class="columns"> <div class="columns">
<!-- Packages block --> <!-- Packages block -->
<div id="packagings" class="size100 no-page-break" v-if="packagings.length > 0"> <div id="packagings" class="size100 no-page-break" v-if="packagings.length > 0">
<h2>{{$t('packagings')}}</h2> <h2>{{$t('packagings.title')}}</h2>
<table class="column-oriented"> <table class="column-oriented">
<thead> <thead>
<tr> <tr>
<th>{{$t('reference')}}</th> <th>{{$t('packagings.theader.reference')}}</th>
<th class="number">{{$t('quantity')}}</th> <th class="number">{{$t('packagings.theader.quantity')}}</th>
<th wihth="75%">{{$t('concept')}}</th> <th wihth="75%">{{$t('packagings.theader.concept')}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -183,21 +184,18 @@
<div class="columns vn-mt-xl"> <div class="columns vn-mt-xl">
<!-- Taxes block --> <!-- Taxes block -->
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes"> <div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
<!-- <h2>{{$t('taxBreakdown')}}</h2> -->
<table class="column-oriented"> <table class="column-oriented">
<thead> <thead>
<tr> <tr>
<th colspan="4">{{$t('taxBreakdown')}}</th> <th colspan="4">{{$t('taxes.title')}}</th>
</tr> </tr>
</thead> </thead>
<thead class="light"> <thead class="light">
<tr> <tr>
<th width="45%">{{$t('type')}}</th> <th width="45%">{{$t('taxes.theader.type')}}</th>
<th width="25%" class="number"> <th width="25%" class="number">{{$t('taxes.theader.taxBase')}}</th>
{{$t('taxBase')}} <th>{{$t('taxes.theader.tax')}}</th>
</th> <th class="number">{{$t('taxes.theader.fee')}}</th>
<th>{{$t('tax')}}</th>
<th class="number">{{$t('fee')}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -12,17 +12,37 @@ price: PSP/u
discount: Disc. discount: Disc.
vat: VAT vat: VAT
amount: Amount amount: Amount
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
total: Total total: Total
subtotal: Subtotal subtotal: Subtotal
taxBreakdown: Tax breakdown
packagings: Buckets and packaging
services: Services
vatType: VAT Type vatType: VAT Type
digitalSignature: Digital signature digitalSignature: Digital signature
ticket: Delivery note {0} ticket: Delivery note {0}
plantPassport: Plant passport plantPassport: Plant passport
packages: Packages packages: Packages
services:
title: Services
theader:
quantity: Qty.
concept: Concept
price: PSP/u
vat: VAT
amount: Amount
tfoot:
subtotal: Subtotal
warning: Deposit packaging will be invoiced if they have not been returned after 30 days of their delivery.
packagings:
title: Buckets and packaging
theader:
reference: Reference
quantity: Quantity
concept: Concept
taxes:
title: Tax breakdown
theader:
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
tfoot:
subtotal: Subtotal
total: Total

View File

@ -12,17 +12,37 @@ price: PVP/u
discount: Dto. discount: Dto.
vat: IVA vat: IVA
amount: Importe amount: Importe
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
total: Total total: Total
subtotal: Subtotal subtotal: Subtotal
taxBreakdown: Desglose impositivo
packagings: Cubos y embalajes
services: Servicios
vatType: Tipo de IVA vatType: Tipo de IVA
digitalSignature: Firma digital digitalSignature: Firma digital
ticket: Albarán {0} ticket: Albarán {0}
plantPassport: Pasaporte fitosanitario plantPassport: Pasaporte fitosanitario
packages: Bultos packages: Bultos
services:
title: Servicios
theader:
quantity: Cantidad
concept: Concepto
price: PVP/u
vat: IVA
amount: Importe
tfoot:
subtotal: Subtotal
warning: Los embalajes en depósito se facturarán si no han sido devueltos pasados 30 dias de su entrega.
packagings:
title: Cubos y embalajes
theader:
reference: Referencia
quantity: Cantidad
concept: Concepto
taxes:
title: Desglose impositivo
theader:
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
tfoot:
subtotal: Subtotal
total: Total

View File

@ -12,17 +12,37 @@ price: PRIX/u
discount: Remise discount: Remise
vat: TVA vat: TVA
amount: Montant amount: Montant
type: Type
taxBase: Base imposable
tax: Taxe
fee: Quote
total: Total total: Total
subtotal: Total partiel subtotal: Total partiel
taxBreakdown: Répartition taxes
packagings: Bacs et emballages
services: Service
vatType: Type de TVA vatType: Type de TVA
digitalSignature: Signature numérique digitalSignature: Signature numérique
ticket: BL {0} ticket: BL {0}
plantPassport: Passeport phytosanitaire plantPassport: Passeport phytosanitaire
packages: Paquets packages: Paquets
services:
title: Service
theader:
quantity: Quantité
concept: Concept
price: PRIX/u
vat: TVA
amount: Montant
tfoot:
subtotal: Total partiel
warning: Les emballages de consigne seront facturés s'ils n'ont pas été retournés après 30 jours de leur livraison.
packagings:
title: Bacs et emballages
theader:
reference: Référence
quantity: Quantité
concept: Concept
taxes:
title: Répartition taxes
theader:
type: Type
taxBase: Base imposable
tax: Taxe
fee: Quote
tfoot:
subtotal: Total partiel
total: Total

View File

@ -12,17 +12,37 @@ price: PVP/u
discount: Dto. discount: Dto.
vat: IVA vat: IVA
amount: Importe amount: Importe
type: Tipo
taxBase: Base imp.
tax: Taxa
fee: Quota
total: Total total: Total
subtotal: Sub-total subtotal: Sub-total
taxBreakdown: Desglose impositivo
packagings: Baldes e Embalagens
services: Serviços
vatType: Tipo de IVA vatType: Tipo de IVA
digitalSignature: Assinatura digital digitalSignature: Assinatura digital
ticket: Nota de Entrega {0} ticket: Nota de Entrega {0}
plantPassport: Passaporte vegetal plantPassport: Passaporte vegetal
packages: Pacotes packages: Pacotes
services:
title: Serviços
theader:
quantity: Quantidade
concept: Conceito
price: PVP/u
vat: IVA
amount: Quantia
tfoot:
subtotal: Subtotal
warning: As embalagens em depósito serão facturadas e cobradas se não são devolvidas 30 dias após a entrega.
packagings:
title: Baldes e Embalagens
theader:
reference: Referência
quantity: Quantidade
concept: Conceito
taxes:
title: Repartição de impostos
theader:
type: Cara
taxBase: Tributável
tax: Taxa
fee: Compartilhado
tfoot:
subtotal: Subtotal
total: Total