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`.`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`)
VALUES
('1', '6');
@ -125,14 +130,14 @@ INSERT INTO `vn`.`warehouseAlias`(`id`, `name`)
(1, 'Main Warehouse'),
(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
(1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1),
(2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13),
(3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
(4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
(5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1),
(13, 'Inventory', NULL, 1, 1, 1, 0, 0, 0, 0, 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, 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, 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, 0);
INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPreparedByPacking`, `code`, `pickingPlacement`, `path`)
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` = 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;
@ -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),
(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),
(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`)
VALUES
@ -2409,4 +2414,6 @@ INSERT INTO `vn`.`expeditionScan` (`id`, `expeditionFk`, `scanned`, `palletFk`)
(7, 7, CURDATE(), 1),
(8, 8, 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()', () => {
it(`should store some data on cache.last_buy`, async() => {
let stmts = [];
let stmt;
const stmts = [];
stmts.push('START TRANSACTION');
stmt = new ParameterizedSQL('CALL cache.last_buy_refresh(true)');
stmts.push(stmt);
let lastBuyTableIndex = stmts.push(`SELECT * FROM cache.last_buy ORDER BY item_id ASC`) - 1;
const lastBuyTableIndex = stmts.push(`SELECT * FROM cache.last_buy ORDER BY item_id ASC`) - 1;
stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await app.models.Ticket.rawStmt(sql);
let lastBuyTable = result[lastBuyTableIndex];
const lastBuyTable = result[lastBuyTableIndex];
expect(lastBuyTable.length).toEqual(12);

View File

@ -186,9 +186,10 @@ export default {
receivedB2BVNLCheckbox: 'vn-client-billing-data vn-check[label="Received B2B VNL"]',
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',
newBankEntityName: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.name"]',
newBankEntityBIC: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.bic"]',
newBankEntityCode: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newBankEntity.id"]',
newBankEntityName: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.data.name"]',
newBankEntityBIC: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.data.bic"]',
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"]',
saveButton: 'vn-client-billing-data button[type=submit]',
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() => {
await page.waitToClick(selectors.clientBillingData.newBankEntityButton);
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.autocompleteSearch(selectors.clientBillingData.newBankEntityCountry, 'España');
await page.write(selectors.clientBillingData.newBankEntityCode, '9999');
await page.waitToClick(selectors.clientBillingData.acceptBankEntityButton);
await page.waitForTextInField(selectors.clientBillingData.swiftBic, 'Gotham City Bank');
const newcode = await page.waitToGetProperty(selectors.clientBillingData.swiftBic, 'value');

View File

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

View File

@ -12,11 +12,12 @@ import './style.scss';
* @event move Emitted when month changes
*/
export default class Calendar extends FormInput {
constructor($element, $scope, vnWeekDays) {
constructor($element, $scope, vnWeekDays, moment) {
super($element, $scope);
this.weekDays = vnWeekDays.locales;
this.defaultDate = new Date();
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.
*/
repaint() {
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());
dayIndex.setDate(1 - weekdayOffset);
dayIndex.setDate(1 - this.weekdayOffset);
this.days = [];
@ -70,27 +79,55 @@ export default class Calendar extends FormInput {
this.days.push(new Date(dayIndex.getTime()));
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.
*
* @param {Date} day The day
* @param {Date} date The date
* @return {Object} The CSS classes to apply
*/
getDayClasses(day) {
let wday = day.getDay();
let month = day.getMonth();
getDayClasses(date) {
let day = date.getDate();
let wday = date.getDay();
let month = date.getMonth();
const currentDay = new Date().getDate();
const currentMonth = new Date().getMonth();
let classes = {
today: day === currentDay && month === currentMonth,
weekend: wday === 6 || wday === 0,
previous: month < this.month,
current: 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;
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', {
template: require('./index.html'),
@ -193,6 +230,7 @@ ngModule.vnComponent('vnCalendar', {
formatDay: '&?',
displayControls: '<?',
hideYear: '<?',
hideContiguous: '<?'
hideContiguous: '<?',
hideWeeks: '<?'
}
});

View File

@ -19,7 +19,14 @@
color: inherit;
}
}
& > .weekdays {
& #days-header {
flex-direction: row;
display: flex
}
& #days-header > .week-numbers {
width: 10%
}
& #days-header > .weekdays {
display: flex;
color: $color-font-secondary;
margin-bottom: 8px;
@ -27,17 +34,49 @@
font-weight: bold;
font-size: .8rem;
text-align: center;
width: 90%;
& > section {
width: 14.28%;
cursor: pointer;
}
}
& > .days {
& #days-header.hide-weeks {
& > .weekdays {
width: 100%
}
}
& > #days-container {
flex-direction: row;
display: flex
}
& > #days-container > .weeks {
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;
align-items: center;
flex-wrap: wrap;
width: 90%;
& > .day {
width: 14.28%;
@ -46,6 +85,17 @@
justify-content: 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 {
color: $color-font-secondary;
}

View File

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

188
front/package-lock.json generated
View File

@ -3,6 +3,181 @@
"version": "1.0.0",
"lockfileVersion": 1,
"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": {
"@uirouter/angularjs": {
"version": "1.0.29",
@ -27,6 +202,14 @@
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"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": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
@ -78,6 +261,11 @@
"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": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",

View File

@ -12,6 +12,7 @@
"@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",

View File

@ -1,40 +1,46 @@
<vn-dialog class="edit"
vn-id="bankEntityDialog"
on-open="$ctrl.resetData()"
on-accept="$ctrl.onAccept()"
message="New bank entity">
<tpl-body>
<p translate>Please, ensure you put the correct data!</p>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
vn-id="entityName"
label="Name"
ng-model="$ctrl.data.name"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
vn-id="bic"
label="Swift"
ng-model="$ctrl.data.bic"
required="true">
</vn-textfield>
<vn-autocomplete vn-one
ng-model="$ctrl.data.countryFk"
url="Countries"
show-field="country"
value-field="id"
label="Country">
</vn-autocomplete>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button id= "saveBankEntity" response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
<tpl-title translate>
New bank entity
</tpl-title>
<tpl-body>
<p translate>Please, ensure you put the correct data!</p>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
vn-id="entityName"
label="Name"
ng-model="$ctrl.data.name"
required="true">
</vn-textfield>
<vn-textfield
vn-one
vn-focus
vn-id="bic"
label="Swift"
ng-model="$ctrl.data.bic"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="country"
ng-model="$ctrl.data.countryFk"
url="Countries"
fields="['id', 'country', 'code']"
show-field="country"
value-field="id"
label="Country">
</vn-autocomplete>
<vn-textfield
vn-one
ng-show="country.selection.code === 'ES'"
label="Entity code"
ng-model="$ctrl.data.id">
</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 Component from 'core/lib/component';
import Dialog from 'core/components/dialog';
import './style.scss';
class Controller extends Component {
open() {
this.$.bankEntityDialog.show();
}
class Controller extends Dialog {
responseHandler(response) {
if (response !== 'accept')
return super.responseHandler(response);
resetData() {
this.data = {};
}
if (!this.data.countryFk)
throw new Error(`The country can't be empty`);
onAccept() {
try {
if (!this.data.countryFk)
throw new Error(`The country can't be empty`);
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;
return this.$http.post(`bankEntities`, this.data)
.then(res => this.data.id = res.data.id)
.then(() => super.responseHandler(response))
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
}
ngModule.vnComponent('vnNewBankEntity', {
template: require('./index.html'),
slotTemplate: require('./index.html'),
controller: Controller,
bindings: {
data: '<',

View File

@ -5,49 +5,35 @@ describe('Salix Component vnNewBankEntity', () => {
let $httpBackend;
let $scope;
let $element;
let vnApp;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_) => {
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
vnApp = _vnApp_;
jest.spyOn(vnApp, 'showError');
$scope = $rootScope.$new();
$element = angular.element('<vn-dialog></dialog>');
controller = $componentController('vnNewBankEntity', {$element, $scope});
$element = angular.element('<vn-dialog></vn-dialog>');
const $transclude = {
$$boundTransclude: {
$$slots: []
}
};
controller = $componentController('vnNewBankEntity', {$element, $scope, $transclude});
controller.vnApp = {showSuccess: jest.fn()};
}));
describe('resetData()', () => {
it('should reset the location in the controller', () => {
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', () => {
describe('responseHandler()', () => {
it('should show a success message after the query to bankEntities', () => {
controller.data = {
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();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.data.id).toEqual(1);
});
});
});

View File

@ -9,4 +9,5 @@ The country can't be empty: El país no puede quedar vacío
The postcode has been created. You can save the data now: Se ha creado el código postal. Ahora puedes guardar los datos
The city has been created: Se ha creado la ciudad
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">
<tpl-body class="upload-photo">
<vn-horizontal>
<vn-one ng-if="file.value">
<vn-one ng-if="file.value || $ctrl.newPhoto.url">
<vn-horizontal>
<vn-icon-button vn-none
icon="rotate_left"
@ -20,12 +20,26 @@
</vn-horizontal>
</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-one
label="File"
ng-model="$ctrl.newPhoto.files"
on-change="$ctrl.updatePhotoPreview(value)"
on-change="$ctrl.updatePhotoPreview(value[0])"
accept="{{$ctrl.allowedContentTypes}}"
required="true">
<append>
@ -37,6 +51,14 @@
</append>
</vn-input-file>
</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-autocomplete
label="Type"

View File

@ -39,6 +39,7 @@ export default class UploadPhoto extends Component {
}
];
this.viewportType = 'normal';
this.uploadMethod = 'computer';
this.getAllowedContentTypes();
}
@ -64,11 +65,16 @@ export default class UploadPhoto extends Component {
set viewportSelection(value) {
this._viewportSelection = value;
if (value && this.newPhoto.files) {
this.displayEditor();
const files = this.newPhoto.files;
this.updatePhotoPreview(files);
}
const hasFile = this.newPhoto.files || this.newPhoto.url;
if (!value || !hasFile) return;
let file;
if (this.uploadMethod == 'computer')
file = this.newPhoto.files[0];
else if (this.uploadMethod == 'URL')
file = this.newPhoto.url;
this.updatePhotoPreview(file);
}
getAllowedContentTypes() {
@ -90,13 +96,15 @@ export default class UploadPhoto extends Component {
* @param {string} value
*/
updatePhotoPreview(value) {
if (value && value[0]) {
if (!this.editor)
this.displayEditor();
if (value) {
this.displayEditor();
const reader = new FileReader();
reader.onload = e => this.editor.bind({url: e.target.result});
reader.readAsDataURL(value[0]);
if (this.uploadMethod == 'computer') {
const reader = new FileReader();
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()', () => {
it('should call to displayEditor() and updatePhotoPreview() methods', () => {
controller.displayEditor = jest.fn();
it('should call to the updatePhotoPreview() method when uploadMethod property is set to "computer"', () => {
controller.updatePhotoPreview = jest.fn();
const files = [{name: 'test.jpg'}];
controller.newPhoto.files = files;
controller.uploadMethod = 'computer';
controller.viewportSelection = {code: 'normal'};
expect(controller.displayEditor).toHaveBeenCalledWith();
expect(controller.updatePhotoPreview).toHaveBeenCalledWith(files);
const firstFile = files[0];
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

@ -3,4 +3,6 @@ Select an image: Selecciona una imagen
File name: Nombre del fichero
Rotate left: Girar a la izquierda
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 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",
"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)
throw new UserError(`Can't verify data unless the client has a business type`);
}
if (changes.credit !== undefined) {
await validateCreditChange(ctx, finalState);
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);
}
// Credit changes
if (changes.credit !== undefined)
await Self.changeCredit(ctx, finalState, changes);
});
Self.observe('after save', async ctx => {
@ -332,42 +322,54 @@ module.exports = Self => {
await models.Chat.send(httpCtx, `@${currentWorker.user}`, message);
};
async function validateCreditChange(ctx, finalState) {
let models = Self.app.models;
let userId = ctx.options.accessToken.userId;
// Credit change validations
Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models;
const userId = ctx.options.accessToken.userId;
let currentUserIsManager = await models.Account.hasRole(userId, 'manager');
if (currentUserIsManager)
return;
const isManager = await models.Account.hasRole(userId, 'manager', ctx.options);
if (!isManager) {
const lastCredit = await models.ClientCredit.findOne({
where: {
clientFk: finalState.id
},
order: 'id DESC'
}, ctx.options);
let filter = {
fields: ['roleFk'],
where: {
maxAmount: {gt: ctx.data.credit}
}
};
const lastAmount = lastCredit && lastCredit.amount;
const lastWorkerId = lastCredit && lastCredit.workerFk;
const lastWorkerIsManager = await models.Account.hasRole(lastWorkerId, 'manager', ctx.options);
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)
throw new UserError('Credit limits not found');
const creditLimits = await models.ClientCreditLimit.find({
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 = [];
for (limit of limits)
requiredRoles.push(limit.roleFk);
const userRequiredRoles = await models.RoleMapping.count({
roleId: {inq: requiredRoles},
principalType: 'USER',
principalId: userId
}, ctx.options);
let where = {
roleId: {inq: requiredRoles},
principalType: 'USER',
principalId: userId
};
let count = await models.RoleMapping.count(where);
if (userRequiredRoles <= 0)
throw new UserError(`You don't have enough privileges to set this credit amount`);
}
if (count <= 0)
throw new UserError('The role cannot set this credit amount');
}
await models.ClientCredit.create({
amount: changes.credit,
clientFk: finalState.id,
workerFk: userId
}, ctx.options);
};
const app = require('vn-loopback/server/server');
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');
describe('Client Model', () => {
@ -14,8 +14,8 @@ describe('Client Model', () => {
}
};
const ctx = {req: activeCtx};
const chatModel = app.models.Chat;
const client = {id: 1101, name: 'Bruce Banner'};
const chatModel = models.Chat;
const instance = {id: 1101, name: 'Bruce Banner'};
const previousWorkerId = 1106; // DavidCharlesHaller
const currentWorkerId = 1107; // HankPym
@ -29,7 +29,7 @@ describe('Client Model', () => {
it('should call to the Chat send() method for both workers', async() => {
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, '@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() => {
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`);
});
@ -46,9 +46,69 @@ describe('Client Model', () => {
it('should call to the Chat send() method for the current worker', async() => {
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`);
});
});
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-auto
icon="add_circle"
ng-click="$ctrl.onAddEntityClick($event)"
vn-click-stop="bankEntity.show({countryFk: $ctrl.client.countryFk})"
vn-tooltip="New bank entity"
vn-acl="salesAssistant">
</vn-icon-button>
@ -108,53 +108,8 @@
</vn-button-bar>
</form>
<!-- Create bank entity dialog -->
<vn-dialog class="edit"
vn-id="bankEntityDialog"
on-accept="$ctrl.onBankEntityAccept()"
message="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>
<!-- New bankentity dialog -->
<vn-new-bank-entity
vn-id="bankEntity"
on-accept="$ctrl.onAccept($data)">
</vn-new-bank-entity>

View File

@ -37,17 +37,8 @@ export default class Controller extends Section {
return payMethod || iban || dueDay;
}
onAddEntityClick(event) {
event.preventDefault();
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);
onAccept(data) {
this.client.bankEntityFk = data.id;
}
get ibanCountry() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,6 +57,11 @@ module.exports = Self => {
arg: 'stemMultiplier',
type: 'integer',
description: 'The item multiplier',
},
{
arg: 'landed',
type: 'date',
description: 'The item last buy landed date',
}
],
returns: {
@ -114,6 +119,8 @@ module.exports = Self => {
return {'ori.code': value};
case 'intrastat':
return {'intr.description': value};
case 'landed':
return {'lb.landed': value};
}
});
@ -146,7 +153,8 @@ module.exports = Self => {
ic.name AS category,
intr.description AS intrastat,
b.grouping,
b.packing
b.packing,
lb.landing AS landed
FROM item i
LEFT JOIN itemType it ON it.id = i.typeFk
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="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
@ -87,6 +88,7 @@
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink-date>{{::item.landed | date:'dd/MM/yyyy'}}</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button

View File

@ -223,7 +223,8 @@ module.exports = Self => {
MINUTE(z.hour) AS zoneMinute,
z.name AS zoneName,
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
LEFT JOIN invoiceOut io ON t.refFk = io.ref
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 client c ON c.id = t.clientFk
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) {
stmt.merge({

View File

@ -6,4 +6,6 @@ Delete selected elements: Eliminar los elementos seleccionados
All the selected elements will be deleted. Are you sure you want to continue?: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar?
Component lack: Faltan componentes
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="salesPersonFk" class="expendable" shrink>Salesperson</vn-th>
<vn-th field="shipped" shrink-date>Date</vn-th>
<vn-th>Hour</vn-th>
<vn-th field="zoneHour" shrink>Closure</vn-th>
<vn-th>Prep.</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="stateFk">State</vn-th>
<vn-th field="zoneFk">Zone</vn-th>
@ -112,6 +113,7 @@
</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.practicalHour | date: 'HH:mm'}}</vn-td>
<vn-td class="expendable">{{::ticket.province}}</vn-td>
<vn-td class="expendable">
<span

View File

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

View File

@ -41,6 +41,14 @@
label="Beneficiary"
ng-model="supplierAccount.beneficiary"
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-none>
<vn-icon-button
@ -67,10 +75,11 @@
</vn-submit>
</vn-button-bar>
</form>
<!-- New bankentity dialog -->
<vn-new-bank-entity
vn-id="bankEntity"
on-response="$ctrl.onResponse($response)">
on-accept="$ctrl.onAccept($data)">
</vn-new-bank-entity>
<vn-confirm

View File

@ -26,27 +26,10 @@ class Controller extends Section {
});
}
onResponse(response) {
const data = this.$.model.data;
const supplierAccount = data[this.currentRowIndex];
supplierAccount.bankEntityFk = response.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());
onAccept(data) {
const accounts = this.supplierAccounts;
const targetAccount = accounts[data.index];
targetAccount.bankEntityFk = data.id;
}
onSubmit() {

View File

@ -5,17 +5,9 @@ import crudModel from 'core/mocks/crud-model';
describe('Supplier Component vnSupplierAccount', () => {
let $scope;
let controller;
let $httpBackend;
let $httpParamSerializer;
let vnApp;
beforeEach(ngModule('supplier'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_, _vnApp_) => {
vnApp = _vnApp_;
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
jest.spyOn(vnApp, 'showError');
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.watcher = watcher;
@ -34,58 +26,20 @@ describe('Supplier Component vnSupplierAccount', () => {
};
}));
describe('showBankEntity()', () => {
it('should do nothing if it default is prevented', () => {
const event = {
defaultPrevented: true,
preventDefault: () => {}
};
jest.spyOn(event, 'preventDefault');
jest.spyOn(controller.$.bankEntity, 'open');
describe('onAccept()', () => {
it('should set the created bank entity id into the target account', () => {
controller.supplierAccounts = [{}, {}, {}];
controller.showBankEntity(event);
expect(event.preventDefault).not.toHaveBeenCalledWith();
expect(controller.$.bankEntity.open).not.toHaveBeenCalledWith();
});
it('should call preventDefault() and open() when the default is not prevented', () => {
const event = {
defaultPrevented: false,
preventDefault: () => {}
const data = {
id: 999,
index: 1
};
jest.spyOn(event, 'preventDefault');
jest.spyOn(controller.$.bankEntity, 'open');
controller.onAccept(data);
controller.showBankEntity(event);
const targetAccount = controller.supplierAccounts[data.index];
expect(event.preventDefault).toHaveBeenCalledWith();
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);
expect(targetAccount.bankEntityFk).toEqual(data.id);
});
});

View File

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

View File

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

View File

@ -7,4 +7,5 @@ of: de
days: días
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
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

@ -41,12 +41,20 @@ vn-worker-calendar {
border-color: rgba(0, 0, 0, 0.3);
border-bottom: 1px solid rgba(0, 0, 0, 0.3);
}
.festive {
background-color:white;
border: 2px solid $color-alert;
vn-avatar.festive,
vn-avatar.today {
background-color: $color-font-dark;
width: 24px;
min-width: 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;
}).finally(async() => {
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, ?, ?, ?)`, [
options.to,
options.replyTo,

View File

@ -143,8 +143,24 @@ module.exports = app => {
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]);
const ticketIds = tickets.map(ticket => ticket.id);
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) {
next(error);
}

View File

@ -2,22 +2,4 @@ subject: Orden de recogida
title: Orden de recogida
description:
dear: Estimado cliente
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.
instructions: Aqui tienes tu orden de recogida.

View File

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

View File

@ -12,17 +12,37 @@ price: PSP/u
discount: Disc.
vat: VAT
amount: Amount
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
total: Total
subtotal: Subtotal
taxBreakdown: Tax breakdown
packagings: Buckets and packaging
services: Services
vatType: VAT Type
digitalSignature: Digital signature
ticket: Delivery note {0}
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.
vat: IVA
amount: Importe
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
total: Total
subtotal: Subtotal
taxBreakdown: Desglose impositivo
packagings: Cubos y embalajes
services: Servicios
vatType: Tipo de IVA
digitalSignature: Firma digital
ticket: Albarán {0}
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
vat: TVA
amount: Montant
type: Type
taxBase: Base imposable
tax: Taxe
fee: Quote
total: Total
subtotal: Total partiel
taxBreakdown: Répartition taxes
packagings: Bacs et emballages
services: Service
vatType: Type de TVA
digitalSignature: Signature numérique
ticket: BL {0}
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.
vat: IVA
amount: Importe
type: Tipo
taxBase: Base imp.
tax: Taxa
fee: Quota
total: Total
subtotal: Sub-total
taxBreakdown: Desglose impositivo
packagings: Baldes e Embalagens
services: Serviços
vatType: Tipo de IVA
digitalSignature: Assinatura digital
ticket: Nota de Entrega {0}
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