Merge branch 'dev' of https://git.verdnatura.es/salix into dev

This commit is contained in:
Javi Gallego 2018-05-16 13:28:59 +02:00
commit 7aa2d04d00
54 changed files with 1326 additions and 353 deletions

View File

@ -1,28 +1,18 @@
<div <div
class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"
tabindex="1"
ng-focus="$ctrl.hasFocus = true" ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false" ng-blur="$ctrl.hasFocus = false"
ng-mouseenter="$ctrl.hasMouseIn = true" ng-mouseenter="$ctrl.hasMouseIn = true"
ng-mouseleave="$ctrl.hasMouseIn = false" ng-mouseleave="$ctrl.hasMouseIn = false">
ng-click="$ctrl.onClick()">
<input type="text" <input type="text"
class="mdl-textfield__input" class="mdl-textfield__input"
name="{{::$ctrl.name}}" name="{{::$ctrl.name}}"
ng-model="$ctrl.modelView"
ng-disabled="{{!$ctrl.enabled}}" ng-disabled="{{!$ctrl.enabled}}"
rule="{{::$ctrl.rule}}"/> rule="{{::$ctrl.rule}}"/>
<div class="mdl-chip__action"> <div class="mdl-chip__action">
<i
class="material-icons"
ng-if="$ctrl.isTimePicker"
ng-click="$ctrl.vp.open()">
query_builder
</i>
<i <i
class="material-icons pointer" class="material-icons pointer"
ng-show="$ctrl.modelView&&($ctrl.hasFocus||$ctrl.hasMouseIn)" ng-show="$ctrl.model && ($ctrl.hasFocus || $ctrl.hasMouseIn)"
ng-click="$ctrl.onClear()"> ng-click="$ctrl.onClear()">
clear clear
</i> </i>

View File

@ -3,240 +3,93 @@ import Component from '../../lib/component';
import Flatpickr from 'vendor/src/flatpickr'; import Flatpickr from 'vendor/src/flatpickr';
import './style.scss'; import './style.scss';
// equivalences to format date between flatpicker and angularjs
export const formatEquivalence = {
d: 'dd', // Day of the month, 2 digits with leading zeros (01 to 31)
j: 'd', // Day of the month without leading zeros (1 to 31)
m: 'MM', // Month in year, padded (01-12)
n: 'M', // Month in year (1-12)
y: 'yy', // A two digit representation of a year (00-99)
Y: 'yyyy', // A full numeric representation of a year, 4 digits (1999 or 2003)
H: 'HH', // Hour in AM/PM, padded (01-12)
h: 'H', // Hour in AM/PM, (1-12)
i: 'mm', // Minutes (00 to 59)
s: 'ss' // Seconds (00 to 59)
};
class DatePicker extends Component { class DatePicker extends Component {
constructor($element, $translate, $filter, $timeout, $attrs) { constructor($element, $scope, $translate, $attrs) {
super($element); super($element, $scope);
this.input = $element[0].querySelector('input'); this.input = $element[0].querySelector('input');
this.$translate = $translate; this.$translate = $translate;
this.$filter = $filter;
this.$timeout = $timeout;
this.$attrs = $attrs; this.$attrs = $attrs;
this.enabled = true; this.enabled = true;
this._modelView = null; this._modelView = null;
this._model = undefined; this._model = undefined;
this._optionsChecked = false; this.dateValue = undefined;
this._waitingInit = 0;
this.hasFocus = false;
this.hasMouseIn = false; this.hasMouseIn = false;
let locale = this.$translate.use();
this.defaultOptions = {
locale: locale,
dateFormat: locale == 'es' ? 'd-m-Y' : 'Y-m-d',
enableTime: false,
onValueUpdate: () => this.onValueUpdate()
};
this.userOptions = {};
this._iniOptions = this.defaultOptions;
componentHandler.upgradeElement($element[0].firstChild); componentHandler.upgradeElement($element[0].firstChild);
this.vp = new Flatpickr(this.input, this._iniOptions);
}
onValueUpdate() {
if (this.vp.selectedDates.length)
this.model = this.vp.selectedDates[0];
else
this.model = null;
this.$.$apply();
}
set iniOptions(value) {
this.userOptions = value;
let options = Object.assign({}, this.defaultOptions, value);
this._iniOptions = options;
this.isTimePicker = options.enableTime && options.noCalendar;
// TODO: When some properties change Flatpickr doesn't refresh the view
//for (let option in options)
// this.vp.set(option, options[option]);
if (this.vp) this.vp.destroy();
this.vp = new Flatpickr(this.input, this._iniOptions);
this.vp.setDate(this.dateValue);
this.mdlUpdate();
}
get iniOptions() {
return this.userOptions;
} }
get model() { get model() {
return this._model; return this._model;
} }
set model(value) {
if (this._optionsChecked) {
this._waitingInit = 0;
this._model = value;
if (value && !this.modelView) {
let options = this._getOptions();
let initialDateFormat;
if (options && options.dateFormat) {
initialDateFormat = options.dateFormat;
} else {
initialDateFormat = this.$translate.use() === 'es' ? 'd-m-Y' : 'Y-m-d';
if (options.enableTime) {
initialDateFormat += ' H:i:s';
}
}
let format = this._formatFlat2Angular(initialDateFormat); set model(value) {
this._modelView = this.$filter('date')(new Date(this._model), format); this._model = value;
this.dateValue = value ? new Date(value) : null;
this.vp.setDate(this.dateValue);
this.mdlUpdate(); this.mdlUpdate();
} }
} else if (this._waitingInit < 4) {
this._waitingInit++;
this.$timeout(() => {
this.model = value;
}, 250);
} else {
this.model = null;
this.modelView = '';
this._waitingInit = 0;
}
}
get modelView() { get modelView() {
return this._modelView; return this._modelView;
} }
set modelView(value) { set modelView(value) {
this._modelView = value; this._modelView = value;
this.input.value = value;
this._setModel(value);
this.mdlUpdate();
} }
onClear() { onClear() {
this.modelView = null; this.model = null;
}
onClick() {
if (this.vp) {
this.vp.open();
}
} }
mdlUpdate() { mdlUpdate() {
this.$timeout(() => {
let mdlField = this.element.firstChild.MaterialTextfield; let mdlField = this.element.firstChild.MaterialTextfield;
if (mdlField) if (mdlField)
mdlField.updateClasses_(); mdlField.updateClasses_();
}, 500);
}
_formatFlat2Angular(string) { // change string Flatpickr format to angular format (d-m-Y -> dd-MM-yyyy)
let aux = string.split(/[ZT.,/ :-]/);
let parts = [];
aux.forEach(
val => {
parts.push(formatEquivalence[val]);
}
);
if (string.indexOf(' ') !== -1 || string.indexOf('T') !== -1) { // datetime format
let dates = parts.slice(0, 3).join('-');
let hours = parts.slice(3, parts.length).join(':');
return `${dates} ${hours}`.trim();
} else if (string.indexOf(':') !== -1) { // only time format
return parts.join(':');
} // only date format
return parts.join('-');
}
_string2BackFormat(value) {
let formats = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/);
let aux = value.split(/[ZT.,/ :-]/);
let date = {};
formats.forEach(
(k, i) => {
if (k.toLowerCase() === 'y') {
date.year = aux[i];
} else if (k === 'm' || k === 'n') {
date.month = aux[i];
} else if (k === 'd' || k === 'j') {
date.day = aux[i];
} else if (k.toLowerCase() === 'h') {
date.hour = aux[i];
} else if (k === 'i') {
date.minutes = aux[i];
} else if (k === 's') {
date.seccons = aux[i];
}
}
);
let dateStr = '';
let hourStr = '';
if (date.year && date.month && date.day) {
dateStr = `${date.year}-${date.month}-${date.day}`;
}
if (date.hour) {
hourStr = date.hour;
if (date.minutes) {
hourStr += ':' + date.minutes;
} else {
hourStr += ':00';
}
if (date.seccons) {
hourStr += ':' + date.seccons;
} else {
hourStr += ':00';
}
}
return `${dateStr} ${hourStr}`.trim();
}
_setModel(value) {
let model;
let options = this._getOptions();
if (!value) {
model = undefined;
} else if (!options || (options.dateFormat && options.dateFormat.startsWith('Y-m-d'))) {
model = value;
} else {
model = this._string2BackFormat(value);
}
if (this.model !== model) {
this.model = model;
}
}
_getOptions() {
if (this.iniOptions && this._optionsChecked) {
return this.iniOptions;
} else if (!this.iniOptions) {
this.iniOptions = {};
}
if (!this.iniOptions.locale)
this.iniOptions.locale = this.$translate.use();
if (!this.iniOptions.dateFormat)
this.iniOptions.dateFormat = this.iniOptions.locale === 'es' ? 'd-m-Y' : 'Y-m-d';
else if (this.iniOptions.dateFormat) {
let format = this.iniOptions.dateFormat.split(/[ZT.,/ :-]/);
if (format.length <= 1) {
throw new Error(`Error: Invalid string format ${format}`);
}
format.forEach(
val => {
if (!formatEquivalence[val]) {
throw new Error(`Error in dateFormat ${this.iniOptions.dateFormat}: is not like Flatpickr Formatting Token https://chmln.github.io/flatpickr/formatting/`);
}
}
);
}
if (this.$attrs.hasOwnProperty('today')) {
this.iniOptions.defaultDate = new Date();
}
this._optionsChecked = true;
return this.iniOptions;
}
initPicker() {
this.iniOptions = this._getOptions();
this.isTimePicker = (this.iniOptions && this.iniOptions.enableTime && this.iniOptions.noCalendar);
this.vp = new Flatpickr(this.input, this.iniOptions);
if (this.iniOptions.defaultDate) {
this.modelView = this.vp.formatDate(this.iniOptions.defaultDate, this.iniOptions.dateFormat);
}
}
destroyPicker() {
if (this.vp)
this.vp.destroy();
this.vp = undefined;
}
$onChanges(objChange) {
if (objChange.iniOptions && objChange.iniOptions.currentValue) {
this.iniOptions = Object.assign(this.iniOptions, objChange.iniOptions.currentValue);
}
}
$onInit() {
this.initPicker();
} }
$onDestroy() { $onDestroy() {
this.destroyPicker(); this.vp.destroy();
} }
} }
DatePicker.$inject = ['$element', '$translate', '$filter', '$timeout', '$attrs']; DatePicker.$inject = ['$element', '$scope', '$translate', '$attrs'];
ngModule.component('vnDatePicker', { ngModule.component('vnDatePicker', {
template: require('./date-picker.html'), template: require('./date-picker.html'),

View File

@ -1,47 +0,0 @@
import './date-picker.js';
describe('Component vnDatePicker', () => {
let $componentController;
let $filter;
let $timeout;
let $element;
let $attrs;
let $translate;
let controller;
beforeEach(() => {
angular.mock.module('client');
});
beforeEach(angular.mock.inject((_$componentController_, _$filter_, _$timeout_, _$translate_) => {
$componentController = _$componentController_;
$filter = _$filter_;
$timeout = _$timeout_;
$element = angular.element(`<div><input></div>`);
$translate = _$translate_;
$attrs = {};
controller = $componentController('vnDatePicker', {$element, $translate, $filter, $timeout, $attrs});
}));
describe('_formatFlat2Angular()', () => {
it(`should format date from Y-m-d to yyyy-MM-dd`, () => {
let formatedDate = controller._formatFlat2Angular(`Y-m-d`);
expect(formatedDate).toBe('yyyy-MM-dd');
});
it(`should format date from d-m-Y to dd-MM-yyyy`, () => {
let formatedDate = controller._formatFlat2Angular(`d-m-Y`);
expect(formatedDate).toBe('dd-MM-yyyy');
});
it(`should split the given string into parts`, () => {
controller.iniOptions = {dateFormat: 'd/m/Y'};
controller._optionsChecked = true;
controller.model = '2017-12-23';
expect(controller.modelView).toBe('23-12-2017');
});
});
});

View File

@ -1,3 +1,5 @@
@import "colors";
vn-date-picker { vn-date-picker {
.mdl-chip__action { .mdl-chip__action {
position: absolute; position: absolute;
@ -7,9 +9,18 @@ vn-date-picker {
margin: 22px 0px; margin: 22px 0px;
background-color: white; background-color: white;
} }
.mdl-textfield {
width: 100%;
}
.material-icons { .material-icons {
font-size: 18px; font-size: 18px;
float: right; float: right;
margin-right: 5px; margin-right: 5px;
} }
} }
.flatpickr-months .flatpickr-month,
.flatpickr-weekdays,
span.flatpickr-weekday {
background-color: $main-01;
}

View File

@ -196,7 +196,7 @@ export default class DropDown extends Component {
} }
let where = {}; let where = {};
where[this.showField] = {regexp: search}; where[this.showField] = {like: `%${search}%`};
return where; return where;
} }

View File

@ -16,13 +16,20 @@ export default class StepControl {
} }
set currentState(state) { set currentState(state) {
let isAllowed = true; if (!this.onStepChange)
return this.$state.go(state);
if (this.onStepChange) let change = this.onStepChange({state});
isAllowed = this.onStepChange({state});
if (typeof change === 'object' && change.then) {
return change.then(isAllowed => {
if (isAllowed) if (isAllowed)
this.$state.go(state); this.$state.go(state);
});
}
if (change)
this.$state.go(state);
} }
get totalSteps() { get totalSteps() {

View File

@ -51,6 +51,7 @@
field="$ctrl.item.expenceFk" field="$ctrl.item.expenceFk"
initial-data="$ctrl.item.expence"> initial-data="$ctrl.item.expence">
</vn-autocomplete> </vn-autocomplete>
<vn-textfield vn-one label="Reference" field="$ctrl.item.description"></vn-textfield>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -0,0 +1 @@
Reference: Referencia

View File

@ -1,4 +1,4 @@
{ /* {
"module": "locator", "module": "locator",
"name": "Locator", "name": "Locator",
"icon": "add_location", "icon": "add_location",
@ -11,4 +11,4 @@
"acl": ["developer"] "acl": ["developer"]
} }
] ]
} } */

View File

@ -2,9 +2,9 @@ auth: []
client: [] client: []
core: [] core: []
item: [] item: []
locator: [] #locator: []
production: [] #production: []
salix: [] salix: []
route: [] #route: []
ticket: [item] ticket: [item]
order: [] order: []

View File

@ -1,4 +1,4 @@
{ /* {
"module": "production", "module": "production",
"name": "Production", "name": "Production",
"icon": "local_florist", "icon": "local_florist",
@ -11,4 +11,4 @@
"acl": ["developer"] "acl": ["developer"]
} }
] ]
} } */

View File

@ -1,4 +1,4 @@
{ /* {
"module": "route", "module": "route",
"name": "Routes", "name": "Routes",
"icon" : "local_shipping", "icon" : "local_shipping",
@ -65,4 +65,4 @@
} }
} }
] ]
} } */

View File

@ -71,6 +71,18 @@
"ticket": "$ctrl.data" "ticket": "$ctrl.data"
} }
}, },
{
"url" : "/sale",
"state": "ticket.card.sale",
"component": "vn-ticket-sale",
"params": {
"ticket": "$ctrl.ticket"
},
"menu": {
"description": "Sale",
"icon": "icon-lines"
}
},
{ {
"url": "/observation", "url": "/observation",
"state": "ticket.card.observation", "state": "ticket.card.observation",
@ -141,7 +153,8 @@
"menu": { "menu": {
"description": "Tracking", "description": "Tracking",
"icon": "remove_red_eye" "icon": "remove_red_eye"
} },
"acl": ["production"]
}, },
{ {
"url": "/edit", "url": "/edit",
@ -159,18 +172,6 @@
"client": "$ctrl.client" "client": "$ctrl.client"
} }
}, },
{
"url" : "/sale",
"state": "ticket.card.sale",
"component": "vn-ticket-sale",
"params": {
"ticket": "$ctrl.ticket"
},
"menu": {
"description": "Sale",
"icon": "icon-lines"
}
},
{ {
"url" : "/sale-checked", "url" : "/sale-checked",
"state": "ticket.card.saleChecked", "state": "ticket.card.saleChecked",

View File

@ -7,17 +7,21 @@ class Controller {
} }
set ticket(data) { set ticket(data) {
if (!data) return;
this.data = Object.assign({}, data); this.data = Object.assign({}, data);
} }
onSubmit() { registerChild(child) {
//post data this.child = child;
alert('Data saved');
console.log(this.data);
} }
onStepChange(state) { onStepChange(state) {
return true; return this.child.onStepChange(state);
}
onSubmit() {
this.child.onSubmit();
} }
} }

View File

@ -0,0 +1 @@
There's no available agency for this landing date: No hay ninguna agencia disponible para la fecha de envío seleccionada

View File

@ -1,6 +1,6 @@
<form name="form"> <form name="form">
<vn-card pad-large> <vn-card pad-large>
<vn-title>Ticket configuration - Basic data</vn-title> <vn-title>Basic data</vn-title>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-one <vn-autocomplete vn-one
url="/api/Clients" url="/api/Clients"
@ -29,14 +29,13 @@
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-date-picker vn-one <vn-date-picker vn-one
label="Shipment" label="Shipped"
model="$ctrl.ticket.shipped" model="$ctrl.ticket.shipped"
ini-options="{enableTime: true, dateFormat: 'd-m-Y h:i', time_24hr: true}"> ini-options="{enableTime: true, dateFormat: 'd-m-Y h:i'}">
</vn-date-picker> </vn-date-picker>
<vn-date-picker vn-one <vn-date-picker vn-one
label="Landing" label="Landed"
model="$ctrl.ticket.landed" model="$ctrl.ticket.landed">
ini-options="{enableTime: true, dateFormat: 'd-m-Y h:i', time_24hr: true}">
</vn-date-picker> </vn-date-picker>
<vn-autocomplete vn-one <vn-autocomplete vn-one
url="/api/Companies" url="/api/Companies"

View File

@ -1,17 +1,58 @@
import ngModule from '../../module'; import ngModule from '../../module';
class Controller { class Controller {
constructor($scope) { constructor($scope, $http, $translate, vnApp) {
this.$scope = $scope; this.$scope = $scope;
this.$http = $http;
this.$translate = $translate;
this.vnApp = vnApp;
}
$onInit() {
this.data.registerChild(this);
}
async onStepChange(state) {
if (this.isFormInvalid())
return this.vnApp.showError(
this.$translate.instant('Some fields are invalid')
);
let query = `/ticket/api/sales/${this.ticket.id}/priceDifference`;
let data = {
landed: this.ticket.landed,
addressFk: this.ticket.addressFk,
agencyModeFk: this.ticket.agencyModeFk
};
return this.$http.post(query, data).then(res => {
if (res.data);
this.ticket.sale = res.data;
return true
}, res => {
if (res.data.error.message === 'NO_AGENCY_AVAILABLE')
this.vnApp.showError(
this.$translate.instant(`There's no available agency for this landing date`)
);
});
}
isFormInvalid() {
return !this.ticket.clientFk || !this.ticket.addressFk || !this.ticket.agencyModeFk
|| !this.ticket.companyFk || !this.ticket.shipped || !this.ticket.landed;
} }
} }
Controller.$inject = ['$scope']; Controller.$inject = ['$scope', '$http', '$translate', 'vnApp'];
ngModule.component('vnTicketDataStepOne', { ngModule.component('vnTicketDataStepOne', {
template: require('./step-one.html'), template: require('./step-one.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
ticket: '<' ticket: '<'
},
require: {
data: '^vnTicketData'
} }
}); });

View File

@ -0,0 +1,63 @@
import './step-one.js';
describe('ticket', () => {
describe('Component vnTicketDataStepOne', () => {
let $componentController;
let $state;
let controller;
let $httpBackend;
beforeEach(() => {
angular.mock.module('ticket');
});
beforeEach(angular.mock.inject((_$componentController_, _$state_, _$httpBackend_) => {
$componentController = _$componentController_;
$state = _$state_;
$httpBackend = _$httpBackend_;
controller = $componentController('vnTicketDataStepOne', {$state: $state});
}));
describe('isFormInvalid()', () => {
it('should check if all form fields are valid', () => {
controller.ticket = {
clientFk: 1,
addressFk: 121,
agencyModeFk: 1,
companyFk: 442,
shipped: Date.now(),
landed: Date.now()
};
let result = controller.isFormInvalid();
expect(result).toBeFalsy();
});
});
describe('onStepChange()', () => {
it('should call onStepChange method and return a NO_AGENCY_AVAILABLE signal error', async () => {
controller.ticket = {
id: 1,
clientFk: 1,
addressFk: 121,
agencyModeFk: 1,
companyFk: 442,
shipped: Date.now(),
landed: Date.now()
};
let data = {
addressFk: 121,
agencyModeFk: 1,
landed: Date.now()
};
let response = {error: {message: 'NO_AGENCY_AVAILABLE'}};
$httpBackend.whenPOST(`/ticket/api/sales/1/priceDifference`, data).respond(400, response);
$httpBackend.expectPOST(`/ticket/api/sales/1/priceDifference`, data);
controller.onStepChange();
$httpBackend.flush();
});
});
});
});

View File

@ -0,0 +1,3 @@
Charge: Cargo
Choose an option: Selecciona una opción
Charge difference to: Diferencia a cargo de

View File

@ -1,14 +1,14 @@
<form name="form"> <form name="form">
<vn-card pad-large> <vn-card pad-large>
<vn-title>Ticket configuration - Charge</vn-title> <vn-title>Charge</vn-title>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-one <vn-autocomplete vn-one
url="/api/Clients" url="/ticket/api/TicketUpdateActions"
label="Charge difference to" label="Charge difference to"
show-field="name" show-field="description"
value-field="id" value-field="id"
field="$ctrl.ticket.clientFk" field="$ctrl.ticket.option"
initial-data="$ctrl.ticket.clientFk"> initial-data="$ctrl.ticket.option">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -1,15 +1,62 @@
import ngModule from '../../module'; import ngModule from '../../module';
class Controller { class Controller {
constructor($scope) {
constructor($http, $scope, $state, $translate, vnApp) {
this.$http = $http;
this.$scope = $scope; this.$scope = $scope;
this.$state = $state;
this.$translate = $translate;
this.vnApp = vnApp;
}
$onInit() {
this.data.registerChild(this);
}
$onChanges() {
this.ticket.option = 1;
}
onStepChange(state) {
return true;
}
onSubmit() {
if (!this.ticket.option)
return this.vnApp.showError(
this.$translate.instant('Choose an option')
);
let query = `/ticket/api/tickets/${this.ticket.id}/componentUpdate`;
let data = {
agencyModeFk: this.ticket.agencyModeFk,
addressFk: this.ticket.addressFk,
warehouseFk: this.ticket.warehouseFk,
shipped: this.ticket.shipped,
landed: this.ticket.landed,
option: this.ticket.option
};
this.$http.post(query, data).then(res => {
if (res.data) {
this.$state.go('ticket.card.summary', {id: this.$state.params.id});
this.card.reload();
}
});
} }
} }
Controller.$inject = ['$http', '$scope', '$state', '$translate', 'vnApp'];
ngModule.component('vnTicketDataStepThree', { ngModule.component('vnTicketDataStepThree', {
template: require('./step-three.html'), template: require('./step-three.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
ticket: '<' ticket: '<'
},
require: {
card: '^vnTicketCard',
data: '^vnTicketData'
} }
}); });

View File

@ -0,0 +1,51 @@
/* import './step-three.js';
describe('ticket', () => {
describe('Component vnTicketDataStepThree', () => {
let $componentController;
let $state;
let controller;
let $httpBackend;
beforeEach(() => {
angular.mock.module('ticket');
});
beforeEach(angular.mock.inject((_$componentController_, _$state_, _$httpBackend_) => {
$componentController = _$componentController_;
$state = _$state_;
$httpBackend = _$httpBackend_;
controller = $componentController('vnTicketDataStepThree', {$state: $state});
}));
describe('onSubmit()', () => {
it('should call onSubmit()', () => {
controller.ticket = {
id: 1,
agencyModeFk: 1,
addressFk: 121,
warehouseFk: 1,
shipped: Date.now(),
landed: Date.now(),
option: 1
};
let data = {
agencyModeFk: 1,
addressFk: 121,
warehouseFk: 1,
shipped: Date.now(),
landed: Date.now(),
option: 1
};
let response = {data: {error: {message: 'NOTHING_HERE'}}};
$httpBackend.whenPOST(`/ticket/api/tickets/1/componentUpdate`, data).respond(400, response);
$httpBackend.expectPOST(`/ticket/api/tickets/1/componentUpdate`, data);
controller.onSubmit();
$httpBackend.flush();
});
});
});
}); */

View File

@ -0,0 +1,3 @@
Price (PPU): Precio (Ud.)
New price (PPU): Nuevo precio (Ud.)
Price difference: Diferencia de precio

View File

@ -1,6 +1,6 @@
<form name="form"> <form name="form">
<vn-card pad-large> <vn-card pad-large>
<vn-title>Ticket configuration - Price difference</vn-title> <vn-title>Price difference</vn-title>
<vn-horizontal> <vn-horizontal>
<table class="vn-grid"> <table class="vn-grid">
<thead> <thead>
@ -8,21 +8,29 @@
<th number translate>Item</th> <th number translate>Item</th>
<th translate style="text-align:center">Description</th> <th translate style="text-align:center">Description</th>
<th number translate>Quantity</th> <th number translate>Quantity</th>
<th number translate>Price</th> <th number translate>Price (PPU)</th>
<th number translate>New price</th> <th number translate>New price (PPU)</th>
<th number translate>Price gap</th> <th number translate>Price difference</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="sale in $ctrl.ticket.sales track by sale.id"> <tr ng-repeat="sale in $ctrl.ticket.sale.items track by sale.id">
<td number>{{("000000"+sale.itemFk).slice(-6)}}</td> <td number>{{("000000"+sale.itemFk).slice(-6)}}</td>
<td><vn-fetched-tags sale="sale"/></td> <td><vn-fetched-tags sale="sale"/></td>
<td number>{{::sale.quantity}}</td> <td number>{{::sale.quantity}}</td>
<td number>{{::sale.price | currency:'€':2}}</td> <td number>{{::sale.price | currency: '€': 2}}</td>
<td number>-</td> <td number>{{::sale.component.newPrice | currency: '€': 2}}</td>
<td number>-</td> <td number>{{::sale.component.difference | currency: '€': 2}}</td>
</tr> </tr>
</tbody> </tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td number><strong>{{$ctrl.ticket.sale.totalUnitPrice | currency: '€': 2}}</strong></td>
<td number><strong>{{$ctrl.ticket.sale.totalNewPrice | currency: '€': 2}}</strong></td>
<td number><strong>{{$ctrl.ticket.sale.totalDifference | currency: '€': 2}}</strong></td>
</tr>
</tfoot>
</table> </table>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -1,28 +1,29 @@
import ngModule from '../../module'; import ngModule from '../../module';
class Controller { class Controller {
constructor($http, $scope) {
constructor($http) {
this.$http = $http; this.$http = $http;
this.$scope = $scope;
} }
$onChanges(data) { $onInit() {
if (!this.ticket || !this.ticket.id) return; this.data.registerChild(this);
}
let query = `/ticket/api/sales/${this.ticket.id}/priceDifference`; onStepChange(state) {
this.$http.get(query).then(res => { return true;
if (res.data)
this.ticket.sales = res.data;
});
} }
} }
Controller.$inject = ['$http', '$scope']; Controller.$inject = ['$http'];
ngModule.component('vnTicketDataStepTwo', { ngModule.component('vnTicketDataStepTwo', {
template: require('./step-two.html'), template: require('./step-two.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
ticket: '<' ticket: '<'
},
require: {
data: '^vnTicketData'
} }
}); });

View File

@ -52,3 +52,4 @@ Warehouse: Almacén
Worker: Trabajador Worker: Trabajador
Package size: Bultos Package size: Bultos
VAT: IVA VAT: IVA
PPU: Ud.

View File

@ -234,7 +234,7 @@ export default {
itemBarcodes: { itemBarcodes: {
barcodeButton: `${components.vnMenuItem}[ui-sref="item.card.itemBarcode"]`, barcodeButton: `${components.vnMenuItem}[ui-sref="item.card.itemBarcode"]`,
addBarcodeButton: `${components.vnIcon}[icon="add_circle"]`, addBarcodeButton: `${components.vnIcon}[icon="add_circle"]`,
thirdCodeInput: `vn-horizontal:nth-child(4) > ${components.vnTextfield}`, thirdCodeInput: `vn-item-barcode vn-horizontal:nth-child(4) > ${components.vnTextfield}`,
submitBarcodesButton: `${components.vnSubmit}`, submitBarcodesButton: `${components.vnSubmit}`,
firstCodeRemoveButton: `vn-horizontal:nth-child(2) > ${components.vnIcon}[icon="remove_circle_outline"]` firstCodeRemoveButton: `vn-horizontal:nth-child(2) > ${components.vnIcon}[icon="remove_circle_outline"]`
}, },

6
package-lock.json generated
View File

@ -5940,9 +5940,9 @@
} }
}, },
"flatpickr": { "flatpickr": {
"version": "2.6.3", "version": "4.4.6",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-2.6.3.tgz", "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.4.6.tgz",
"integrity": "sha1-RXNXUy3rE189pktCW/RDVzeWFWQ=" "integrity": "sha512-5b4aJtMBiyXyg5paf3lZ872t1Qjt7Qv4SNqChKh2AvP+OYaC1jrutty0goW6WvvkASoamzeFEFbPJIBdHqxNRA=="
}, },
"for-in": { "for-in": {
"version": "0.1.6", "version": "0.1.6",

View File

@ -15,7 +15,7 @@
"angular-paging": "^2.2.2", "angular-paging": "^2.2.2",
"angular-translate": "^2.17.0", "angular-translate": "^2.17.0",
"angular-translate-loader-partial": "^2.17.0", "angular-translate-loader-partial": "^2.17.0",
"flatpickr": "^2.6.3", "flatpickr": "^4.4.6",
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",
"material-design-lite": "^1.3.0", "material-design-lite": "^1.3.0",
"mg-crud": "^1.1.2", "mg-crud": "^1.1.2",

View File

@ -0,0 +1,73 @@
USE `vn`;
DROP procedure IF EXISTS `agencyHourOffer`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `agencyHourOffer`(vDate DATE, vAddressFk INT, vAgencyFk INT)
BEGIN
/**
* Devuelve las posibles fechas de envío de un ticket
*
* @param vDate La fecha
* @param vAddressFk Id del consignatario
* @param vAgencyFk Id de la agencia
* @return tmp.agencyHourOffer
*/
DECLARE vDone BOOL;
DECLARE vWarehouseFk SMALLINT;
DECLARE vCur CURSOR FOR
SELECT w.id warehouseFk
FROM warehouse w
WHERE w.hasAvailable;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DROP TEMPORARY TABLE IF EXISTS tmp.agencyHourOffer;
CREATE TEMPORARY TABLE tmp.agencyHourOffer
(
warehouseFk TINYINT NOT NULL PRIMARY KEY,
shipped DATE NOT NULL,
landed DATE NOT NULL
)
ENGINE = MEMORY;
OPEN vCur;
l: LOOP
SET vDone = FALSE;
FETCH vCur INTO vWarehouseFk;
IF vDone THEN
LEAVE l;
END IF;
INSERT INTO tmp.agencyHourOffer (warehouseFk, shipped, landed)
SELECT vWarehouseFk, shipping, vDate FROM (
SELECT TIMESTAMPADD(DAY, -ah.substractDay, vDate) shipping, ah.maxHour
FROM agencyHour ah
LEFT JOIN address a ON a.id = vAddressFk
WHERE ah.warehouseFk = vWarehouseFk
AND (weekDay = WEEKDAY(vDate)
OR weekDay IS NULL)
AND (ah.agencyFk = vAgencyFk
OR ah.agencyFk IS NULL)
AND (ah.provinceFk = a.provinceFk
OR ah.provinceFk IS NULL
OR vAddressFk IS NULL)
ORDER BY (
(ah.weekDay IS NOT NULL) +
(ah.agencyFk IS NOT NULL) +
((ah.provinceFk IS NOT NULL) * 3)
) DESC
LIMIT 1
) t
WHERE t.shipping >= CURDATE()
AND IF(t.shipping = CURDATE(), t.maxHour > HOUR(NOW()), TRUE);
END LOOP;
CLOSE vCur;
END$$
DELIMITER ;

View File

@ -0,0 +1,43 @@
USE `vn`;
DROP procedure IF EXISTS `buyUltimateFromInterval`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `buyUltimateFromInterval`(vWarehouseFk SMALLINT, vStarted DATE, vEnded DATE)
BEGIN
/**
* Calcula las últimas compras realizadas
* desde un rango de fechas.
*
* @param vWarehouseFk Id del almacén
* @param vStarted Fecha inicial
* @param vEnded Fecha fin
* @return tmp.buyUltimateFromInterval
*/
IF vEnded IS NULL THEN
SET vEnded = vStarted;
END IF;
IF vEnded < vStarted THEN
SET vStarted = TIMESTAMPADD(MONTH, -1, vEnded);
END IF;
DROP TEMPORARY TABLE IF EXISTS tmp.buyUltimateFromInterval;
CREATE TEMPORARY TABLE tmp.buyUltimateFromInterval
ENGINE = MEMORY
SELECT
b.itemFk,
t.warehouseInFk warehouseFk,
MULTIMAX(t.landed, b.id) buyFk,
MAX(t.landed) landed
FROM buy b
JOIN entry e ON e.id = b.entryFk
JOIN travel t ON t.id = e.travelFk
WHERE t.landed BETWEEN vStarted AND vEnded
AND (vWarehouseFk IS NULL OR t.warehouseInFk = vWarehouseFk)
AND b.price2 > 0
AND NOT b.isIgnored
GROUP BY itemFk, warehouseInFk;
END$$
DELIMITER ;

View File

@ -0,0 +1,35 @@
USE `vn`;
DROP procedure IF EXISTS `buyUltimate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `buyUltimate`(vWarehouseFk SMALLINT, vDate DATE)
BEGIN
/**
* Calcula las últimas compras realizadas hasta una fecha
*
* @param vWarehouseFk Id del almacén
* @param vDate Compras hasta fecha
* @return tmp.buyUltimate
*/
CALL cache.last_buy_refresh (FALSE);
DROP TEMPORARY TABLE IF EXISTS tmp.buyUltimate;
CREATE TEMPORARY TABLE tmp.buyUltimate
(PRIMARY KEY (itemFk, warehouseFk))
ENGINE = MEMORY
SELECT item_id itemFk, buy_id buyFk, warehouse_id warehouseFk
FROM cache.last_buy
WHERE warehouse_id = vWarehouseFk OR vWarehouseFk IS NULL;
CALL vn.buyUltimateFromInterval(vWarehouseFk, CURDATE(), vDate);
REPLACE INTO tmp.buyUltimate
SELECT itemFk, buyFk, warehouseFk
FROM tmp.buyUltimateFromInterval
WHERE warehouseFk = vWarehouseFk OR vWarehouseFk IS NULL;
DROP TEMPORARY TABLE tmp.buyUltimateFromInterval;
END$$
DELIMITER ;

View File

@ -0,0 +1,294 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentCalculate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticketComponentCalculate`(
vAddressFk INT,
vAgencyModeFk INT)
proc: BEGIN
/**
* Calcula los componentes de un ticket
*
* @param vAddressFk Id del consignatario
* @param vAgencyModeFk Id del modo de agencia
* @return tmp.ticketComponent, tmp.ticketComponentPrice
*/
DECLARE vClientFk INT;
DECLARE vGeneralInflationCoefficient INT DEFAULT 1.3;
DECLARE vMinimumDensityWeight INT DEFAULT 167;
DECLARE vBoxFreightItem INT DEFAULT 71;
DECLARE vBoxVolume BIGINT DEFAULT 138000;
DECLARE vSpecialPriceComponent INT DEFAULT 10;
DECLARE vExtraFreightComponent INT DEFAULT 14;
DECLARE vDeliveryComponent INT DEFAULT 15;
DECLARE vRecoveryComponent INT DEFAULT 17;
DECLARE vSellByPacketComponent INT DEFAULT 22;
DECLARE vBuyValueComponent INT DEFAULT 28;
DECLARE vMarginComponent INT DEFAULT 29;
DECLARE vDiscountLastItemComponent INT DEFAULT 32;
DECLARE vExtraBaggedComponent INT DEFAULT 38;
DECLARE vManaAutoComponent INT DEFAULT 39;
DECLARE vFreightBonusComponent INT DEFAULT 41;
SELECT clientFk INTO vClientFK
FROM address
WHERE id = vAddressFk;
SET @rate2 := 0;
SET @rate3 := 0;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentCalculate;
CREATE TEMPORARY TABLE tmp.ticketComponentCalculate
(PRIMARY KEY (itemFk, warehouseFk))
ENGINE = MEMORY
SELECT
tl.itemFk, tl.warehouseFk, tl.available,
IF((@rate2 := IFNULL(pf.rate2, b.price2)) < i.minPrice AND i.hasMinPrice, i.minPrice, @rate2) * 1.0 rate2,
IF((@rate3 := IFNULL(pf.rate3, b.price3)) < i.minPrice AND i.hasMinPrice, i.minPrice, @rate3) * 1.0 rate3,
IFNULL(pf.rate3, 0) AS minPrice,
IFNULL(pf.packing, b.packing) packing,
IFNULL(pf.grouping, b.grouping) grouping,
ABS(IFNULL(pf.box, b.groupingMode)) groupingMode,
tl.buyFk, i.typeFk
FROM tmp.ticketLot tl
JOIN buy b ON b.id = tl.buyFk
JOIN item i ON i.id = tl.itemFk
JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN specialPrice sp ON sp.itemFk = i.id AND sp.clientFk = vClientFk
LEFT JOIN (
SELECT * FROM (
SELECT pf.itemFk, pf.grouping, pf.packing, pf.box, pf.rate2, pf.rate3, aho.warehouseFk
FROM priceFixed pf
JOIN tmp.agencyHourOffer aho ON pf.warehouseFk = aho.warehouseFk OR pf.warehouseFk = 0
WHERE aho.shipped BETWEEN pf.started AND pf.ended ORDER BY pf.itemFk, pf.warehouseFk DESC
) tpf
GROUP BY tpf.itemFk, tpf.warehouseFk
) pf ON pf.itemFk = tl.itemFk AND pf.warehouseFk = tl.warehouseFk
WHERE b.buyingValue + b.freightValue + b.packageValue + b.comissionValue > 0.01 AND ic.display <> 0;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponent;
CREATE TEMPORARY TABLE tmp.ticketComponent (
`warehouseFk` INT UNSIGNED NOT NULL,
`itemFk` INT NOT NULL,
`componentFk` INT UNSIGNED NOT NULL,
`cost` DECIMAL(10,4) NOT NULL,
INDEX `itemWarehouse` USING BTREE (`itemFk` ASC, `warehouseFk` ASC),
UNIQUE INDEX `itemWarehouseComponent` (`itemFk` ASC, `warehouseFk` ASC, `componentFk` ASC));
INSERT INTO tmp.ticketComponent (warehouseFk, itemFk, componentFk, cost)
SELECT
tcc.warehouseFk,
tcc.itemFk,
vBuyValueComponent,
tcc.rate3 - b.buyingValue - b.freightValue - b.packageValue - b.comissionValue
FROM tmp.ticketComponentCalculate tcc
JOIN buy b ON b.id = tcc.buyFk;
INSERT INTO tmp.ticketComponent (warehouseFk, itemFk, componentFk, cost)
SELECT
tcc.warehouseFk,
tcc.itemFk,
vMarginComponent,
tcc.rate3 - b.buyingValue - b.freightValue - b.packageValue - b.comissionValue
FROM tmp.ticketComponentCalculate tcc
JOIN buy b ON b.id = tcc.buyFk;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentBase;
CREATE TEMPORARY TABLE tmp.ticketComponentBase ENGINE = MEMORY
SELECT tc.itemFk, ROUND(SUM(tc.cost), 4) AS base, tc.warehouseFk
FROM tmp.ticketComponent tc
GROUP BY tc.itemFk, warehouseFk;
INSERT INTO tmp.ticketComponent
SELECT tcb.warehouseFk, tcb.itemFk, vRecoveryComponent, ROUND(tcb.base * LEAST(cr.recobro, 0.25), 3)
FROM tmp.ticketComponentBase tcb
JOIN bi.claims_ratio cr ON cr.Id_Cliente = vClientFk
WHERE cr.recobro > 0.009;
INSERT INTO tmp.ticketComponent
SELECT tcb.warehouseFk, tcb.itemFk, vManaAutoComponent, ROUND(base * (0.01 + prices_modifier_rate), 3) as manaAuto
FROM tmp.ticketComponentBase tcb
JOIN `client` c on c.id = vClientFk
JOIN bs.mana_spellers ms ON c.salesPersonFk = ms.Id_Trabajador
WHERE ms.prices_modifier_activated
HAVING manaAuto <> 0;
INSERT INTO tmp.ticketComponent
SELECT
tcb.warehouseFk,
tcb.itemFk,
cr.id,
GREATEST(IFNULL(ROUND(tcb.base * cr.tax, 4), 0), tcc.minPrice - tcc.rate3)
FROM tmp.ticketComponentBase tcb
JOIN componentRate cr
JOIN tmp.ticketComponentCalculate tcc ON tcc.itemFk = tcb.itemFk AND tcc.warehouseFk = tcb.warehouseFk
LEFT JOIN specialPrice sp ON sp.clientFk = vClientFk AND sp.itemFk = tcc.itemFk
WHERE cr.id = vDiscountLastItemComponent AND cr.tax <> 0 AND tcc.minPrice < tcc.rate3 AND sp.value IS NULL;
INSERT INTO tmp.ticketComponent
SELECT tcc.warehouseFk, tcc.itemFk, vSellByPacketComponent, tcc.rate2 - tcc.rate3
FROM tmp.ticketComponentCalculate tcc
JOIN buy b ON b.id = tcc.buyFk
LEFT JOIN specialPrice sp ON sp.clientFk = vClientFk AND sp.itemFk = tcc.itemFk
WHERE sp.value IS NULL;
INSERT INTO tmp.ticketComponent
SELECT
tcc.warehouseFK,
tcc.itemFk,
vDeliveryComponent,
vGeneralInflationCoefficient
* ROUND(
r.cm3
* IF(am.deliveryMethodFk = 1, (GREATEST(i.density, vMinimumDensityWeight) / vMinimumDensityWeight), 1)
* IFNULL(amz.price
* amz.inflation, 50) / vBoxVolume, 4
) cost
FROM tmp.ticketComponentCalculate tcc
JOIN item i ON i.id = tcc.itemFk
JOIN agencyMode am ON am.id = vAgencyModeFk
JOIN `address` a ON a.id = vAddressFk
JOIN agencyProvince ap ON ap.agencyFk = a.id
AND ap.warehouseFk = tcc.warehouseFk AND ap.provinceFk = a.provinceFk
JOIN agencyModeZone amz ON amz.agencyModeFk = vAgencyModeFk
AND amz.zone = ap.zone AND amz.itemFk = 71 AND amz.warehouseFk = tcc.warehouseFk
LEFT JOIN bi.rotacion r ON r.warehouse_id = tcc.warehouseFk
AND r.Id_Article = tcc.itemFk
HAVING cost <> 0;
INSERT INTO tmp.ticketComponent
SELECT
tcc.warehouseFk,
tcc.itemFk,
vFreightBonusComponent,
vGeneralInflationCoefficient
* ROUND(
r.cm3
* IF(am.deliveryMethodFk = 1, (GREATEST(i.density, vMinimumDensityWeight) / vMinimumDensityWeight), 1)
* awb.bonus
* amz.inflation / vBoxVolume, 4
) cost
FROM tmp.ticketComponentCalculate tcc
JOIN item i ON i.id = tcc.itemFk
JOIN agencyMode am ON am.id = vAgencyModeFk
JOIN `address` a ON a.id = vAddressFk
JOIN agencyProvince ap ON ap.agencyFk = a.id
AND ap.warehouseFk = tcc.warehouseFk AND ap.provinceFk = a.provinceFk
JOIN agencyModeZone amz ON amz.agencyModeFk = vAgencyModeFk
AND amz.zone = ap.zone AND amz.itemFk = vBoxFreightItem AND amz.warehouseFk = tcc.warehouseFk
JOIN agencyWeekDayBonus awb ON awb.warehouseFk = amz.warehouseFk AND awb.zone = amz.zone AND am.id = awb.agencyFk
LEFT JOIN bi.rotacion r ON r.warehouse_id = tcc.warehouseFk
AND r.Id_Article = tcc.itemFk
JOIN tmp.agencyHourOffer aho ON aho.warehouseFk = awb.warehouseFk
AND WEEKDAY(aho.landed) = awb.weekDay
HAVING cost <> 0
LIMIT 1;
IF (SELECT COUNT(*) FROM vn.addressForPackaging WHERE addressFk = vAddressFk) THEN
INSERT INTO tmp.ticketComponent
SELECT tcc.warehouseFk, b.itemFk, vExtraBaggedComponent, ap.packagingValue cost
FROM tmp.ticketComponentCalculate tcc
JOIN vn.addressForPackaging ap
WHERE ap.addressFk = vAddressFk;
END IF;
INSERT INTO tmp.ticketComponent
SELECT tcb.warehouseFk, tcb.itemFk, vExtraFreightComponent, tcb.base * (IFNULL(pe.percentage,pp.percentage)/100)
FROM tmp.ticketComponentBase tcb
JOIN tmp.agencyHourOffer aho ON aho.warehouseFk = tcb.warehouseFk
LEFT JOIN preparationPercentage pp ON pp.weekDay = WEEKDAY(aho.shipped)
AND tcb.warehouseFk = IFNULL(pp.warehouseFk, tcb.warehouseFk)
LEFT JOIN preparationException pe ON pe.exceptionDay = aho.shipped
AND tcb.warehouseFk = IFNULL(pe.warehouseFk, tcb.warehouseFk)
WHERE IFNULL(pe.percentage, pp.percentage);
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentCopy;
CREATE TEMPORARY TABLE tmp.ticketComponentCopy ENGINE = MEMORY
SELECT * FROM tmp.ticketComponent;
INSERT INTO tmp.ticketComponent
SELECT
tcc.warehouseFk,
tcc.itemFk,
vSpecialPriceComponent,
sp.value - SUM(tcc.cost) sumCost
FROM tmp.ticketComponentCopy tcc
JOIN componentRate cr ON cr.id = tcc.componentFk
JOIN specialPrice sp ON sp.clientFk = vClientFK AND sp.itemFk = tcc.itemFk
WHERE cr.classRate IS NULL
GROUP BY tcc.itemFk, tcc.warehouseFk
HAVING ABS(sumCost) > 0.001;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentSum;
CREATE TEMPORARY TABLE tmp.ticketComponentSum
(INDEX (itemFk, warehouseFk))
ENGINE = MEMORY
SELECT SUM(cost) sumCost, tc.itemFk, tc.warehouseFk, cr.classRate
FROM tmp.ticketComponent tc
JOIN componentRate cr ON cr.id = tc.componentFk
GROUP BY tc.itemFk, tc.warehouseFk, cr.classRate;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentRate;
CREATE TEMPORARY TABLE tmp.ticketComponentRate ENGINE = MEMORY
SELECT
tcc.warehouseFk,
tcc.itemFk,
1 rate,
IF(tcc.groupingMode = 1, tcc.grouping, 1) grouping,
SUM(tcs.sumCost) price
FROM tmp.ticketComponentCalculate tcc
JOIN tmp.ticketComponentSum tcs ON tcs.itemFk = tcc.itemFk
AND tcs.warehouseFk = tcc.warehouseFk
WHERE IFNULL(tcs.classRate, 1) = 1
AND tcc.groupingMode < 2 AND (tcc.packing > tcc.grouping or tcc.groupingMode = 0)
GROUP BY tcs.warehouseFk, tcs.itemFk;
INSERT INTO tmp.ticketComponentRate (warehouseFk, itemFk, rate, grouping, price)
SELECT
tcc.warehouseFk,
tcc.itemFk,
2 rate,
tcc.packing grouping,
SUM(tcs.sumCost) price
FROM tmp.ticketComponentCalculate tcc
JOIN tmp.ticketComponentSum tcs ON tcs.itemFk = tcc.itemFk
AND tcs.warehouseFk = tcc.warehouseFk
WHERE tcc.available IS NULL OR (IFNULL(tcs.classRate, 2) = 2
AND tcc.packing > 0 AND tcc.available >= tcc.packing)
GROUP BY tcs.warehouseFk, tcs.itemFk;
INSERT INTO tmp.ticketComponentRate (warehouseFk, itemFk, rate, grouping, price)
SELECT
tcc.warehouseFk,
tcc.itemFk,
3 rate,
tcc.available grouping,
SUM(tcs.sumCost) price
FROM tmp.ticketComponentCalculate tcc
JOIN tmp.ticketComponentSum tcs ON tcs.itemFk = tcc.itemFk
AND tcs.warehouseFk = tcc.warehouseFk
WHERE IFNULL(tcs.classRate, 3) = 3
GROUP BY tcs.warehouseFk, tcs.itemFk;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentPrice;
CREATE TEMPORARY TABLE tmp.ticketComponentPrice ENGINE = MEMORY
SELECT * FROM (
SELECT * FROM tmp.ticketComponentRate ORDER BY price
) t
GROUP BY itemFk, warehouseFk, grouping;
DROP TEMPORARY TABLE
tmp.ticketComponentCalculate,
tmp.ticketComponentSum,
tmp.ticketComponentBase,
tmp.ticketComponentRate,
tmp.ticketComponentCopy;
END$$
DELIMITER ;

View File

@ -0,0 +1,77 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentPreview`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticketComponentPreview`(
vDate DATE,
vAddressFk INT,
vAgencyModeFk INT,
vTicketFk INT)
BEGIN
/**
* Devuelve un listado previo de
* componentes para un ticket
*
* @param vDate Fecha de envío
* @param vAddressFk Id del consignatario
* @param vAgencyModeFk Id del modo de agencia
* @param vTicketFk Id del ticket
*/
DECLARE vWarehouseFk SMALLINT;
DECLARE vAgencyFk INT;
DECLARE vShipped DATE;
DECLARE vBuyOrderItem INT DEFAULT 100;
SELECT warehouseFk INTO vWarehouseFK
FROM ticket
WHERE id = vTicketFk;
SELECT agencyFk INTO vAgencyFk
FROM agencyMode
WHERE id = vAgencyModeFk;
CALL agencyHourOffer(vDate, vAddressFk, vAgencyFk);
SELECT shipped INTO vShipped
FROM tmp.agencyHourOffer
WHERE warehouseFk = vWarehouseFK;
CALL buyUltimate(vWarehouseFK, vShipped);
DROP TEMPORARY TABLE IF EXISTS tmp.ticketLot;
CREATE TEMPORARY TABLE tmp.ticketLot ENGINE = MEMORY (
SELECT
vWarehouseFK AS warehouseFk,
NULL AS available,
s.itemFk,
bu.buyFk
FROM sale s
LEFT JOIN tmp.buyUltimate bu ON bu.itemFk = s.itemFk
WHERE s.ticketFk = vTicketFk
AND s.itemFk != vBuyOrderItem
GROUP BY bu.warehouseFk, bu.itemFk);
CALL ticketComponentCalculate(vAddressFk, vAgencyModeFk);
REPLACE INTO tmp.ticketComponent (warehouseFk, itemFk, componentFk, cost)
SELECT t.warehouseFk, s.itemFk, sc.componentFk, sc.value
FROM saleComponent sc
JOIN sale s ON s.id = sc.saleFk
JOIN ticket t ON t.id = s.ticketFk
JOIN componentRate cr ON cr.id = sc.componentFk
WHERE s.ticketFk = vTicketFk AND NOT cr.isRenewable;
SET @shipped = vShipped;
DROP TEMPORARY TABLE
tmp.agencyHourOffer,
tmp.buyUltimate,
tmp.ticketLot;
IF IFNULL(vShipped, CURDATE() - 1) < CURDATE() THEN
CALL util.throw('NO_AGENCY_AVAILABLE');
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,50 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentPriceDifference`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticketComponentPriceDifference`(
vDate DATE,
vAddressFk INT,
vAgencyModeFk INT,
vTicketFk INT)
BEGIN
/**
* Devuelve las diferencias de precio
* de los movimientos de un ticket.
*
* @param vTicketFk Id del ticket
*/
CALL vn.ticketComponentPreview(vDate, vAddressFk, vAgencyModeFk, vTicketFk);
SELECT
s.itemFk,
i.name,
i.size,
i.category,
IFNULL(s.quantity, 0) AS quantity,
IFNULL(s.price, 0) AS price,
ROUND(SUM(tc.cost), 4) AS newPrice,
s.quantity * (s.price - ROUND(SUM(cost), 4)) difference,
s.id AS saleFk
FROM sale s
JOIN item i ON i.id = s.itemFk
JOIN ticket t ON t.id = s.ticketFk
LEFT JOIN tmp.ticketComponent tc ON tc.itemFk = s.itemFk
AND tc.warehouseFk = t.warehouseFk
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
AND sc.componentFk = tc.componentFk
LEFT JOIN componentRate cr ON cr.id = tc.componentFk
WHERE
t.id = vTicketFk
AND IF(sc.componentFk IS NULL
AND cr.classRate IS NOT NULL, FALSE, TRUE)
GROUP BY s.id ORDER BY s.id;
DROP TEMPORARY TABLE
tmp.ticketComponent,
tmp.ticketComponentPrice;
END$$
DELIMITER ;

View File

@ -0,0 +1,50 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentUpdate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticketComponentUpdate`(
vTicketFk INT,
vAgencyModeFk INT,
vAddressFk INT,
vWarehouseFk INT,
vShipped DATETIME,
vLanded DATE,
vOption INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
UPDATE ticket t
SET
t.agencyModeFk = vAgencyModeFk,
t.addressFk = vAddressFk,
t.warehouseFk = vWarehouseFk,
t.landed = vLanded,
t.shipped = vShipped
WHERE
t.id = vTicketFk;
IF vOption <> 8 THEN
DROP TEMPORARY TABLE IF EXISTS tmp.sale;
CREATE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (saleFk))
ENGINE = MEMORY
SELECT id AS saleFk, vWarehouseFk warehouseFk
FROM sale s WHERE s.ticketFk = vTicketFk;
CALL ticketComponentUpdateSale (vOption);
DROP TEMPORARY TABLE tmp.sale;
END IF;
COMMIT;
END$$
DELIMITER ;

View File

@ -0,0 +1,153 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentUpdateSale`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticketComponentUpdateSale`(vOption INT)
BEGIN
/**
* A partir de la tabla tmp.movement, crea los Movimientos_componentes
* y modifica el campo Preu de la tabla Movimientos
*
* @param i_option integer tipo de actualizacion
* @param table tmp.movement tabla memory con el campo Id_Movimiento, warehouse_id
**/
DECLARE vComponentFk INT;
DECLARE vRenewComponents BOOLEAN;
DECLARE vKeepPrices BOOLEAN;
CASE vOption
WHEN 1 THEN
SET vRenewComponents = TRUE;
SET vKeepPrices = FALSE;
WHEN 2 THEN
SET vComponentFk = 17;
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
WHEN 3 THEN
SET vComponentFk = 37;
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
WHEN 4 THEN
SET vComponentFk = 34;
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
WHEN 5 THEN
SET vComponentFk = 35;
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
WHEN 6 THEN
SET vComponentFk = 36;
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
WHEN 7 THEN
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 28, ROUND(((s.price * (100 - s.discount) / 100) - SUM(IFNULL(sc.value, 0))) * 0.8, 3)
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
AND sc.componentFk NOT IN (28, 29)
GROUP BY s.id;
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 29, ROUND(((s.price * (100 - s.discount) / 100) - SUM(IFNULL(sc.value, 0))) * 0.2, 3)
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
AND sc.componentFk NOT IN (28, 29)
GROUP BY s.id;
SET vRenewComponents = FALSE;
SET vKeepPrices = FALSE;
WHEN 8 THEN
DELETE sc.*
FROM tmp.sale tmps JOIN saleComponent sc ON sc.saleFk = tmps.saleFk;
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 28, ROUND(((s.price * (100 - s.discount) / 100)), 3)
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id;
SET vRenewComponents = FALSE;
SET vKeepPrices = FALSE;
WHEN 9 THEN
SET vRenewComponents = TRUE;
SET vKeepPrices = TRUE;
END CASE;
IF vRenewComponents THEN
DELETE sc.*
FROM tmp.sale tmps
JOIN saleComponent sc ON sc.saleFk = tmps.saleFk
JOIN componentRate cr ON cr.id = sc.componentFk
WHERE cr.isRenewable;
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, tc.componentFk, tc.cost
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN tmp.ticketComponent tc ON tc.itemFk = s.itemFk AND tc.warehouseFk = tmps.warehouseFk
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
AND sc.componentFk = tc.componentFk
LEFT JOIN componentRate cr ON cr.id = tc.componentFk
WHERE IF(sc.componentFk IS NULL AND NOT cr.isRenewable, FALSE, TRUE);
END IF;
IF vKeepPrices THEN
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, vComponentFk, ROUND((s.price * (100 - s.discount) / 100) - SUM(sc.value), 3) dif
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
WHERE sc.saleFk <> vComponentFk
GROUP BY s.id
HAVING dif <> 0;
ELSE
UPDATE sale s
JOIN item i on i.id = s.id
JOIN itemType it on it.id = i.typeFk
JOIN (SELECT SUM(sc.value) sumValue, sc.saleFk
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
GROUP BY sc.saleFk) sc ON sc.saleFk = s.id
SET s.price = sumValue
WHERE it.code != 'PRT';
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 21, s.price * (100 - s.discount) / 100 - sum(sc.value) saleValue
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
WHERE sc.saleFk != 21
GROUP BY s.id
HAVING ROUND(saleValue, 4) <> 0;
END IF;
UPDATE sale s
JOIN (
SELECT SUM(sc.value) sumValue, sc.saleFk
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
JOIN componentRate cr ON cr.id = sc.componentFk
JOIN componentTypeRate ctr on ctr.id = cr.componentTypeRate AND ctr.base
GROUP BY sc.saleFk) sc ON sc.saleFk = s.id
SET s.priceFixed = sumValue, s.isPriceFixed = 1;
DELETE sc.*
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
JOIN sale s on s.id = sc.saleFk
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE it.code = 'PRT';
INSERT INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 15, s.price
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN item i ON i.id = s.itemFK
JOIN itemType it ON it.id = i.typeFk
WHERE it.code = 'PRT' AND s.price > 0;
END$$
DELIMITER ;

View File

@ -0,0 +1,31 @@
USE `vn`;
DROP procedure IF EXISTS `ticketComponentMakeUpdate`;
DELIMITER $$
USE `vn`$$
CREATE PROCEDURE `ticketComponentMakeUpdate` (
vTicketFk INT,
vAgencyModeFk INT,
vAddressFk INT,
vWarehouseFk INT,
vShipped DATETIME,
vLanded DATE,
vOption INT)
BEGIN
/**
* Devuelve las diferencias de precio
* de los movimientos de un ticket.
*
* @param vTicketFk Id del ticket
*/
CALL vn.ticketComponentPreview (vLanded, vAddressFk, vAgencyModeFk, vTicketFk);
CALL vn.ticketComponentUpdate (vTicketFk, vAgencyModeFk, vAddressFk, vWarehouseFk, vShipped, vLanded, vOption);
DROP TEMPORARY TABLE
tmp.ticketComponent,
tmp.ticketComponentPrice;
END$$
DELIMITER ;

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn2008`.`Tickets`
ADD COLUMN `isDeleted` TINYINT(2) NOT NULL DEFAULT '0' AFTER `boxed`;

View File

@ -0,0 +1,26 @@
USE `vn`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `ticket` AS
SELECT
`t`.`Id_Ticket` AS `id`,
`t`.`Id_Cliente` AS `clientFk`,
`t`.`warehouse_id` AS `warehouseFk`,
`t`.`Fecha` AS `shipped`,
`t`.`landing` AS `landed`,
`t`.`Alias` AS `nickname`,
`t`.`Factura` AS `refFk`,
`t`.`Id_Consigna` AS `addressFk`,
`t`.`Localizacion` AS `location`,
`t`.`Solucion` AS `solution`,
`t`.`Id_Ruta` AS `routeFk`,
`t`.`empresa_id` AS `companyFk`,
`t`.`Id_Agencia` AS `agencyModeFk`,
`t`.`Prioridad` AS `priority`,
`t`.`Bultos` AS `packages`,
`t`.`isDeleted`,
`t`.`odbc_date` AS `created`
FROM
`vn2008`.`Tickets` `t`;

View File

@ -17,5 +17,7 @@
"The IBAN does not have the correct format": "El IBAN no tiene el formato correcto", "The IBAN does not have the correct format": "El IBAN no tiene el formato correcto",
"That payment method requires an IBAN": "El método de pago seleccionado requiere que se especifique el IBAN", "That payment method requires an IBAN": "El método de pago seleccionado requiere que se especifique el IBAN",
"State cannot be blank": "El estado no puede estar en blanco", "State cannot be blank": "El estado no puede estar en blanco",
"Cannot change the payment method if no salesperson": "No se puede cambiar la forma de pago si no hay comercial asignado" "Cannot change the payment method if no salesperson": "No se puede cambiar la forma de pago si no hay comercial asignado",
"EXPIRED_DATE": "EXPIRED_DATE",
"NO_AGENCY_AVAILABLE": "NO_AGENCY_AVAILABLE"
} }

View File

@ -37,6 +37,7 @@ module.exports = Self => {
delete copy.id; delete copy.id;
delete copy.itemTag; delete copy.itemTag;
delete copy.description;
let newItem = await Self.create(copy); let newItem = await Self.create(copy);
let promises = []; let promises = [];

View File

@ -1,6 +1,6 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('priceDifference', { Self.remoteMethod('priceDifference', {
description: 'Returns a sale price difference', description: 'Returns sales with price difference',
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'ticketFk', arg: 'ticketFk',
@ -8,6 +8,12 @@ module.exports = Self => {
required: true, required: true,
description: 'ticket id', description: 'ticket id',
http: {source: 'path'} http: {source: 'path'}
}, {
arg: 'data',
type: 'Object',
required: true,
description: 'landed, addressFk, agencyModeFk',
http: {source: 'body'}
}], }],
returns: { returns: {
type: ['Object'], type: ['Object'],
@ -15,11 +21,11 @@ module.exports = Self => {
}, },
http: { http: {
path: `/:ticketFk/priceDifference`, path: `/:ticketFk/priceDifference`,
verb: 'GET' verb: 'post'
} }
}); });
Self.priceDifference = async ticketFk => { Self.priceDifference = async (ticketFk, data) => {
let filter = { let filter = {
where: { where: {
ticketFk: ticketFk ticketFk: ticketFk
@ -45,7 +51,26 @@ module.exports = Self => {
} }
}] }]
}; };
let salesObj = {};
salesObj.items = await Self.find(filter);
salesObj.totalUnitPrice = 0.00;
salesObj.totalNewPrice = 0.00;
salesObj.totalDifference = 0.00;
return await Self.find(filter); let query = `CALL vn.ticketComponentPriceDifference(?, ?, ?, ?)`;
let [differences] = await Self.rawSql(query, [data.landed, data.addressFk, data.agencyModeFk, ticketFk]);
salesObj.items.forEach(sale => {
differences.forEach(difference => {
if (sale.id == difference.saleFk)
sale.component = difference;
});
salesObj.totalUnitPrice += sale.price;
salesObj.totalNewPrice += sale.component.newPrice;
salesObj.totalDifference += sale.component.difference;
});
return salesObj;
}; };
}; };

View File

@ -0,0 +1,16 @@
const app = require(`${servicesDir}/ticket/server/server`);
describe('sale priceDifference()', () => {
it('should call the priceDifference method and return a NO_AGENCY_AVAILABLE signal error', done => {
let data = {
landed: Date.now(),
addressFk: 121,
agencyModeFk: 1
};
app.models.Sale.priceDifference(1, data)
.catch(response => {
expect(response).toEqual(new Error('ER_SIGNAL_EXCEPTION: NO_AGENCY_AVAILABLE'));
done();
});
});
});

View File

@ -0,0 +1,41 @@
module.exports = Self => {
Self.remoteMethod('componentUpdate', {
description: 'Save ticket sale components',
accessType: 'WRITE',
accepts: [{
arg: 'ticketFk',
type: 'number',
required: true,
description: 'ticket id',
http: {source: 'path'}
}, {
arg: 'data',
type: 'Object',
required: true,
description: 'landed, addressFk, agencyModeFk',
http: {source: 'body'}
}],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/:ticketFk/componentUpdate`,
verb: 'post'
}
});
Self.componentUpdate = async (ticketFk, data) => {
let query = 'CALL vn.ticketComponentMakeUpdate(?, ?, ?, ?, ?, ?, ?)';
let res = await Self.rawSql(query, [
ticketFk,
data.agencyModeFk,
data.addressFk,
data.warehouseFk,
data.shipped,
data.landed,
data.option
]);
return res;
};
};

View File

@ -0,0 +1,19 @@
const app = require(`${servicesDir}/ticket/server/server`);
describe('ticket componentUpdate()', () => {
it('should call the componentUpdate method', done => {
let data = {
agencyModeFk: 1,
addressFk: 121,
warehouseFk: 1,
shipped: Date.now(),
landed: Date.now(),
option: 1
};
app.models.Ticket.componentUpdate(1, data)
.catch(response => {
expect(response).toEqual(new Error('ER_SIGNAL_EXCEPTION: NO_AGENCY_AVAILABLE'));
done();
});
});
});

View File

@ -6,4 +6,5 @@ module.exports = Self => {
require('../methods/ticket/summary')(Self); require('../methods/ticket/summary')(Self);
require('../methods/ticket/getTotal')(Self); require('../methods/ticket/getTotal')(Self);
require('../methods/ticket/getTaxes')(Self); require('../methods/ticket/getTaxes')(Self);
require('../methods/ticket/componentUpdate')(Self);
}; };

View File

@ -12,7 +12,8 @@
"nodemailer": "^4.0.1", "nodemailer": "^4.0.1",
"path": "^0.12.7", "path": "^0.12.7",
"request": "^2.83.0", "request": "^2.83.0",
"require-yaml": "0.0.1" "require-yaml": "0.0.1",
"fs-extra": "^5.0.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -10,7 +10,9 @@
"inline-css": "^2.2.2", "inline-css": "^2.2.2",
"mustache": "^2.3.0", "mustache": "^2.3.0",
"mysql": "^2.13.0", "mysql": "^2.13.0",
"path": "^0.12.7" "path": "^0.12.7",
"require-yaml": "0.0.1",
"fs-extra": "^5.0.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -8,14 +8,6 @@ module.exports = function(Self) {
let token = ctx.options.accessToken; let token = ctx.options.accessToken;
let userId = token && token.userId; let userId = token && token.userId;
let isEmployee = await models.Account.hasRole(userId, 'employee');
let isProduction = await models.Account.hasRole(userId, 'production');
let isAlertLevelZero = await models.State.isAlertLevelZero(ctx.instance.stateFk);
let ticketAlertLevel = await models.TicketState.findOne({where: {id: ctx.instance.ticketFk}, fields: ["alertLevel"]});
if ((!isProduction && !isAlertLevelZero) || !isEmployee || (isEmployee && ticketAlertLevel != 0 && !isProduction))
throw new Error("You don't have enough privileges to do that");
let user = await models.Worker.findOne({where: {userFk: userId}}); let user = await models.Worker.findOne({where: {userFk: userId}});
ctx.instance.workerFk = user.id; ctx.instance.workerFk = user.id;
}); });