Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2145_e2e_linux_fix

This commit is contained in:
Bernat 2020-03-13 12:03:57 +01:00
commit 91b58eb56b
31 changed files with 312 additions and 74 deletions

View File

@ -823,7 +823,7 @@ export default {
travelThermograph: {
add: 'vn-travel-thermograph-index vn-float-button[icon="add"]',
thermographID: 'vn-travel-thermograph-create vn-autocomplete[ng-model="$ctrl.dms.thermographId"]',
uploadIcon: 'vn-travel-thermograph-create vn-icon[icon="attach_file"]',
uploadIcon: 'vn-travel-thermograph-create vn-icon[icon="icon-attach"]',
createdThermograph: 'vn-travel-thermograph-index vn-tbody > vn-tr',
upload: 'vn-travel-thermograph-create button[type=submit]'
},

View File

@ -13,7 +13,7 @@ export default class Datalist extends Textfield {
this._selection = null;
this.buildInput('text');
this.input.setAttribute('autocomplete', 'off');
this.autocomplete = 'off';
}
get field() {
@ -258,7 +258,8 @@ export default class Datalist extends Textfield {
fragment.appendChild(option);
}
list.appendChild(fragment);
this.$.$applyAsync(() =>
list.appendChild(fragment));
}
}

View File

@ -100,7 +100,9 @@ export default class Watcher extends Component {
*/
submit() {
try {
this.check();
if (this.requestMethod() !== 'post')
this.check();
else this.isInvalid();
} catch (err) {
return this.$q.reject(err);
}
@ -120,12 +122,12 @@ export default class Watcher extends Component {
if (this.form)
this.form.$setSubmitted();
if (!this.dataChanged()) {
const isPost = (this.requestMethod() === 'post');
if (!this.dataChanged() && !isPost) {
this.updateOriginalData();
return this.$q.resolve();
}
let isPost = (this.$attrs.save && this.$attrs.save.toLowerCase() === 'post');
let changedData = isPost
? this.data
: getModifiedData(this.data, this.orgData);
@ -158,7 +160,6 @@ export default class Watcher extends Component {
});
}
return this.$q((resolve, reject) => {
this.$http.post(this.url, changedData).then(
json => this.writeData(json, resolve),
@ -166,6 +167,13 @@ export default class Watcher extends Component {
);
});
}
/**
* return the request method.
*/
requestMethod() {
return this.$attrs.save && this.$attrs.save.toLowerCase();
}
/**
* Checks if data is ready to send.
@ -177,6 +185,14 @@ export default class Watcher extends Component {
throw new UserError('No changes to save');
}
/**
* Checks if the form is valid.
*/
isInvalid() {
if (this.form && this.form.$invalid)
throw new UserError('Some fields are invalid');
}
/**
* Notifies the user that the data has been saved.
*/

View File

@ -60,7 +60,7 @@ describe('Directive http-click', () => {
}).finally(() => {
expect(finalValue).toEqual('called!');
}).catch(err => {
console.log(err);
throw err;
});
});
});

View File

@ -2,10 +2,11 @@ import ngModule from '../module';
import getMainRoute from '../lib/get-main-route';
export default class Modules {
constructor(aclService, $window) {
constructor(aclService, $window, $translate) {
Object.assign(this, {
aclService,
$window
$window,
$translate
});
}
@ -17,7 +18,7 @@ export default class Modules {
if (this.modules)
return this.modules;
this.modules = [];
const modules = [];
for (let mod of this.$window.routes) {
if (!mod || !mod.routes) continue;
@ -31,7 +32,7 @@ export default class Modules {
if (res) keyBind = res.key.toUpperCase();
}
this.modules.push({
modules.push({
name: mod.name || mod.module,
icon: mod.icon || null,
route,
@ -39,9 +40,16 @@ export default class Modules {
});
}
const sortedModules = modules.sort((a, b) => {
const translatedNameA = this.$translate.instant(a.name);
const translatedNameB = this.$translate.instant(b.name);
return translatedNameA.localeCompare(translatedNameB);
});
this.modules = sortedModules;
return this.modules;
}
}
Modules.$inject = ['aclService', '$window'];
Modules.$inject = ['aclService', '$window', '$translate'];
ngModule.service('vnModules', Modules);

View File

@ -8,7 +8,7 @@
<div>
<vn-icon icon="{{::mod.icon || 'photo'}}"></vn-icon>
</div>
<h4 ng-bind-html="::$ctrl.getModuleName(mod)"></h4>
<h4 ng-bind-html="$ctrl.getModuleName(mod)"></h4>
<span
ng-show="::mod.keyBind"
vn-tooltip="Ctrl + Alt + {{::mod.keyBind}}">

View File

@ -128,5 +128,6 @@
"Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}",
"ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto",
"Distance must be lesser than 1000": "La distancia debe ser inferior a 1000",
"This ticket is deleted": "Este ticket está eliminado"
"This ticket is deleted": "Este ticket está eliminado",
"A travel with this data already exists": "Ya existe un travel con estos datos"
}

View File

@ -175,5 +175,12 @@
"model": "ItemNiche",
"foreignKey": "itemFk"
}
},
"scope": {
"where": {
"name": {
"neq": ""
}
}
}
}

View File

@ -16,7 +16,8 @@ class Controller extends Component {
{name: 'Send Delivery Note', callback: this.confirmDeliveryNote},
{name: 'Delete ticket', callback: this.showDeleteTicketDialog},
{name: 'Change shipped hour', callback: this.showChangeShipped},
{name: 'Send SMS', callback: this.showSMSDialog},
{name: 'SMS Pending payment', callback: this.sendPaymentSms},
{name: 'SMS Minimum import', callback: this.sendImportSms},
{
name: 'Add stowaway',
callback: this.showAddStowaway,
@ -240,17 +241,30 @@ class Controller extends Component {
);
}
sendImportSms() {
const params = {
ticketId: this.ticket.id,
created: this.ticket.created
};
const message = this.$params.message || this.$translate.instant('Minimum is needed', params);
this.newSMS = {message};
this.showSMSDialog();
}
sendPaymentSms() {
const message = this.$params.message || this.$translate.instant('Make a payment');
this.newSMS = {message};
this.showSMSDialog();
}
showSMSDialog() {
const address = this.ticket.address;
const client = this.ticket.client;
const phone = this.$params.phone || address.mobile || address.phone ||
client.mobile || client.phone;
const message = this.$params.message || this.$translate.instant('SMSPayment');
this.newSMS = {
destinationFk: this.ticket.clientFk,
destination: phone,
message: message
};
this.newSMS.destinationFk = this.ticket.clientFk;
this.newSMS.destination = phone;
this.$.sms.open();
}

View File

@ -20,7 +20,13 @@ describe('Ticket Component vnTicketDescriptor', () => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnTicketDescriptor', {$element});
controller._ticket = {id: 2, invoiceOut: {id: 1}, client: {id: 101, email: 'client@email'}};
controller._ticket = {
id: 2,
clientFk: 101,
invoiceOut: {id: 1},
client: {id: 101, email: 'client@email'},
address: {id: 101, mobile: 111111111, phone: 2222222222}
};
controller.cardReload = ()=> {
return true;
};
@ -161,7 +167,6 @@ describe('Ticket Component vnTicketDescriptor', () => {
});
});
describe('showAddStowaway()', () => {
it('should show a dialog with a list of tickets available for an stowaway', () => {
controller.$.addStowaway = {};
@ -223,4 +228,20 @@ describe('Ticket Component vnTicketDescriptor', () => {
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
});
});
describe('showSMSDialog()', () => {
it('should set the destionationFk and destination properties and then call the sms open() method', () => {
controller.$.sms = {open: () => {}};
jest.spyOn(controller.$.sms, 'open');
const clientId = 101;
const expectedPhone = 111111111;
controller.newSMS = {};
controller.showSMSDialog();
expect(controller.newSMS.destinationFk).toEqual(clientId);
expect(controller.newSMS.destination).toEqual(expectedPhone);
expect(controller.$.sms.open).toHaveBeenCalledWith();
});
});
});

View File

@ -1,3 +1,2 @@
SMSPayment: >-
Verdnatura communicates: Your order is pending of payment.
Please, enter the web page and make the payment with card. Thank you.
Make a payment: "Verdnatura communicates:\rYour order is pending of payment.\rPlease, enter the web page and make the payment with card.\rThank you."
Minimum is needed: "Verdnatura communicates:\rA minimum import of 50€ (Without BAT) is needed for your order {{ticketId}} from date {{created | date: 'dd/MM/yyyy'}} to receive it with no extra fees."

View File

@ -13,7 +13,8 @@ Send Delivery Note: Enviar albarán
Show pallet report: Ver hoja de pallet
Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío
SMSPayment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectue el pago con tarjeta.\rMuchas gracias."
Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPor favor, entre en la página web y efectue el pago con tarjeta.\rMuchas gracias."
Minimum is needed: "Verdnatura le recuerda:\rEs necesario llegar a un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales."
Ticket invoiced: Ticket facturado
Make invoice: Crear factura
Regenerate invoice: Regenerar factura
@ -25,4 +26,6 @@ Invoice sent for a regeneration, will be available in a few minutes: La factura
Shipped hour updated: Hora de envio modificada
Deleted ticket: Ticket eliminado
Recalculate components: Recalcular componentes
Are you sure you want to recalculate the components?: ¿Seguro que quieres recalcular los componentes?
Are you sure you want to recalculate the components?: ¿Seguro que quieres recalcular los componentes?
SMS Minimum import: 'SMS Importe minimo'
SMS Pending payment: 'SMS Pago pendiente'

View File

@ -2,4 +2,5 @@ Weekly tickets: Tickets programados
Go to lines: Ir a lineas
Not available: No disponible
Payment on account...: Pago a cuenta...
Closure: Cierre
Closure: Cierre
You cannot make a payment on account from multiple clients: No puedes realizar un pago a cuenta de clientes diferentes

View File

@ -105,20 +105,22 @@
on-error-src/>
</vn-td>
<vn-td vn-focus number>
<span class="link" ng-if="sale.itemFk"
<span class="link" ng-if="sale.id"
ng-click="$ctrl.showDescriptor($event, sale.itemFk)">
{{sale.itemFk | zeroFill:6}}
{{sale.itemFk}}
</span>
<vn-autocomplete
ng-if="!sale.itemFk"
ng-if="!sale.id"
vn-focus
vn-one
url="Items"
ng-model="sale.itemFk"
show-field="name"
value-field="id"
search-function="{or: [{id: $search}, {name: {like: '%' + $search + '%'}}]}"
order="id DESC">
search-function="$ctrl.itemSearchFunc($search)"
on-change="$ctrl.onChangeQuantity(sale)"
order="id DESC"
tabindex="1">
<tpl-item>
{{id}} - {{name}}
</tpl-item>
@ -137,7 +139,8 @@
<vn-td ng-if="!sale.id" number>
<vn-input-number
ng-model="sale.quantity"
on-change="$ctrl.onChangeQuantity(sale)">
on-change="$ctrl.onChangeQuantity(sale)"
tabindex="2">
</vn-input-number>
</vn-td>
<vn-td-editable disabled="!sale.id || !$ctrl.isEditable" expand>
@ -150,7 +153,7 @@
</vn-fetched-tags>
</text>
<field>
<vn-textfield
<vn-textfield class="dense"
vn-id="concept"
ng-model="sale.concept"
on-change="$ctrl.updateConcept(sale)">
@ -167,7 +170,8 @@
<vn-td number>
<span ng-class="{'link': !$ctrl.isLocked}"
title="{{!$ctrl.isLocked ? 'Edit discount' : ''}}"
ng-click="$ctrl.showEditDiscountPopover($event, sale)">
ng-click="$ctrl.showEditDiscountPopover($event, sale)"
ng-if="sale.id">
{{(sale.discount / 100) | percentage}}
</span>
</vn-td>

View File

@ -12,7 +12,7 @@ class Controller {
this.edit = {};
this.moreOptions = [
{
name: 'Send SMS',
name: 'Send shortage SMS',
callback: this.showSMSDialog
}, {
name: 'Mark as reserved',
@ -182,7 +182,6 @@ class Controller {
return checkedLines.length;
}
removeCheckedLines() {
const sales = this.checkedLines();
@ -448,7 +447,7 @@ class Controller {
this.newSMS = {
destinationFk: this.ticket.clientFk,
destination: phone,
message: this.$translate.instant('SMSAvailability', params)
message: this.$translate.instant('Product not available', params)
};
this.$scope.sms.open();
}
@ -465,10 +464,12 @@ class Controller {
* Updates the sale quantity for existing instance
*/
onChangeQuantity(sale) {
if (!sale.quantity) return;
if (!sale.id)
this.addSale(sale);
else
this.updateQuantity(sale);
return this.addSale(sale);
this.updateQuantity(sale);
}
/*
@ -561,6 +562,12 @@ class Controller {
this.$scope.model.refresh();
});
}
itemSearchFunc($search) {
return /^\d+$/.test($search)
? {id: $search}
: {name: {like: '%' + $search + '%'}};
}
}
Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate'];

View File

@ -1,3 +1,3 @@
SMSAvailability: >-
Product not available: >-
Verdnatura communicates: Your order {{ticketFk}} created on {{created | date: "dd/MM/yyyy"}}.
{{notAvailables}} not available. Sorry for the inconvenience.

View File

@ -24,7 +24,8 @@ Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
Change ticket state to 'Ok': Cambiar estado del ticket a 'Ok'
Reserved: Reservado
SMSAvailability: "Verdnatura le comunica:\rPedido {{ticketFk}} día {{created | date: 'dd/MM/yyyy'}}.\r{{notAvailables}} no disponible/s.\rDisculpe las molestias."
Send shortage SMS: Enviar SMS faltas
Product not available: "Verdnatura le comunica:\rPedido {{ticketFk}} día {{created | date: 'dd/MM/yyyy'}}.\r{{notAvailables}} no disponible/s.\rDisculpe las molestias."
Continue anyway?: ¿Continuar de todas formas?
This ticket is now empty: El ticket ha quedado vacio
Do you want to delete it?: ¿Quieres eliminarlo?

View File

@ -207,6 +207,39 @@ describe('Ticket', () => {
});
});
describe('onChangeQuantity()', () => {
it('should not call addSale() or updateQuantity() methods', () => {
jest.spyOn(controller, 'addSale');
jest.spyOn(controller, 'updateQuantity');
const sale = {itemFk: 4};
controller.onChangeQuantity(sale);
expect(controller.addSale).not.toHaveBeenCalled();
expect(controller.updateQuantity).not.toHaveBeenCalled();
});
it('should call addSale() method', () => {
jest.spyOn(controller, 'addSale');
const sale = {itemFk: 4, quantity: 5};
controller.onChangeQuantity(sale);
expect(controller.addSale).toHaveBeenCalledWith(sale);
});
it('should call updateQuantity() method', () => {
jest.spyOn(controller, 'updateQuantity');
jest.spyOn(controller, 'addSale');
const sale = {id: 1, itemFk: 4, quantity: 5};
controller.onChangeQuantity(sale);
expect(controller.addSale).not.toHaveBeenCalled();
expect(controller.updateQuantity).toHaveBeenCalledWith(sale);
});
});
describe('updateQuantity()', () => {
it('should make a POST query saving sale quantity', () => {
jest.spyOn(controller.$scope.watcher, 'updateOriginalData');

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
require('../methods/travel/getTravel')(Self);
require('../methods/travel/getEntries')(Self);
@ -5,4 +7,10 @@ module.exports = Self => {
require('../methods/travel/createThermograph')(Self);
require('../methods/travel/deleteThermograph')(Self);
require('../methods/travel/updateThermograph')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError('A travel with this data already exists');
return err;
});
};

View File

@ -2,6 +2,16 @@ import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $, $stateParams) {
super($element, $);
this.$stateParams = $stateParams;
}
$onChanges() {
if (this.$stateParams && this.$stateParams.q)
this.travel = JSON.parse(this.$stateParams.q);
}
onSubmit() {
return this.$.watcher.submit().then(
res => this.$state.go('travel.card.summary', {id: res.data.id})
@ -9,6 +19,8 @@ class Controller extends Section {
}
}
Controller.$inject = ['$element', '$scope', '$stateParams'];
ngModule.component('vnTravelCreate', {
template: require('./index.html'),
controller: Controller

View File

@ -26,5 +26,18 @@ describe('Travel Component vnTravelCreate', () => {
expect(controller.$state.go).toHaveBeenCalledWith('travel.card.summary', {id: 1234});
});
});
describe('$onChanges()', () => {
it('should update the travel data when stateParams.q is defined', () => {
controller.$stateParams = {q: '{"ref": 1,"agencyModeFk": 1}'};
const params = {q: '{"ref": 1, "agencyModeFk": 1}'};
const json = JSON.parse(params.q);
controller.$onChanges();
expect(controller.travel).toEqual(json);
});
});
});

View File

@ -41,12 +41,19 @@
<vn-td expand>{{::travel.warehouseInName}}</vn-td>
<vn-td center>{{::travel.landed | date:'dd/MM/yyyy'}}</vn-td>
<vn-td center><vn-check ng-model="travel.isReceived" disabled="true"></vn-check></vn-td>
<vn-td>
<vn-icon-button
ng-click="$ctrl.preview($event, travel)"
vn-tooltip="Preview"
icon="desktop_windows">
</vn-icon-button>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
ng-click="$ctrl.cloneTravel($event, travel)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
ng-click="$ctrl.preview($event, travel)"
vn-tooltip="Preview"
icon="desktop_windows">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
@ -65,4 +72,10 @@
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
vn-id="clone"
on-response="$ctrl.onCloneAccept($response)"
question="Do you want to clone this travel?"
message="All it's properties will be copied">
</vn-confirm>
<vn-scroll-up></vn-scroll-up>

View File

@ -1,16 +1,10 @@
import ngModule from '../module';
export default class Controller {
constructor($scope) {
constructor($scope, $state) {
this.$ = $scope;
this.ticketSelected = null;
}
preview(event, travel) {
this.travelSelected = travel;
this.$.summary.show();
event.preventDefault();
event.stopImmediatePropagation();
this.$state = $state;
}
getScopeDates(days) {
@ -35,9 +29,44 @@ export default class Controller {
} else
this.$.model.clear();
}
stopEvent(event) {
event.preventDefault();
event.stopImmediatePropagation();
}
cloneTravel(event, travel) {
this.stopEvent(event);
this.travelSelected = travel;
this.$.clone.show();
}
onCloneAccept(response) {
if (!(response == 'accept' && this.travelSelected))
return;
if (this.travelSelected) {
const travel = {
ref: this.travelSelected.ref,
agencyModeFk: this.travelSelected.agencyFk,
shipped: this.travelSelected.shipped,
landed: this.travelSelected.landed,
warehouseInFk: this.travelSelected.warehouseInFk,
warehouseOutFk: this.travelSelected.warehouseOutFk
};
const queryParams = JSON.stringify(travel);
this.$state.go('travel.create', {q: queryParams});
}
this.travelSelected = null;
}
preview(event, travel) {
this.stopEvent(event);
this.travelSelected = travel;
this.$.summary.show();
}
}
Controller.$inject = ['$scope'];
Controller.$inject = ['$scope', '$state'];
ngModule.component('vnTravelIndex', {
template: require('./index.html'),

View File

@ -61,4 +61,48 @@ describe('Travel Component vnTravelIndex', () => {
expect(range - dayInMilliseconds).toEqual(dayInMilliseconds + millisecondsPerAddedDay);
});
});
describe('onCloneAccept()', () => {
it('should do nothing if response is not accept', () => {
jest.spyOn(controller.$state, 'go');
let response = 'ERROR!';
controller.travelSelected = 'check me';
controller.onCloneAccept(response);
expect(controller.$state.go).not.toHaveBeenCalledWith();
expect(controller.travelSelected).toEqual('check me');
});
it('should do nothing if response is accept but travelSelected is not defined in the controller', () => {
jest.spyOn(controller.$state, 'go');
let response = 'accept';
controller.travelSelected = undefined;
controller.onCloneAccept(response);
expect(controller.$state.go).not.toHaveBeenCalledWith();
expect(controller.travelSelected).toBeUndefined();
});
it('should call go() then update travelSelected in the controller', () => {
jest.spyOn(controller.$state, 'go');
let response = 'accept';
controller.travelSelected = {
ref: 1,
agencyFk: 1};
const travel = {
ref: controller.travelSelected.ref,
agencyModeFk: controller.travelSelected.agencyFk
};
const queryParams = JSON.stringify(travel);
controller.onCloneAccept(response);
expect(controller.$state.go).toHaveBeenCalledWith('travel.create', {q: queryParams});
expect(controller.travelSelected).toBeNull();
});
});
});

View File

@ -0,0 +1,3 @@
Do you want to clone this travel?: ¿Desea clonar este envio?
All it's properties will be copied: Todas sus propiedades serán copiadas
Clone: Clonar

View File

@ -54,7 +54,7 @@
"component": "vn-travel-log",
"description": "Log"
}, {
"url": "/create",
"url": "/create?q",
"state": "travel.create",
"component": "vn-travel-create",
"description": "New travel"

View File

@ -23,12 +23,12 @@ module.exports = Self => {
beginningYear.setHours(0, 0, 0, 0);
let holidays = await Self.rawSql(
`SELECT lh.dated, chn.name, cht.name, w.id
FROM vn.holiday lh
JOIN vn.workCenter w ON w.id = lh.workcenterFk
LEFT JOIN vn.calendarHolidaysName chn ON chn.id = lh.holidayDetailFk
LEFT JOIN vn.calendarHolidaysType cht ON cht.id = lh.holidayTypeFk
WHERE w.warehouseFk = ? AND lh.dated >= ?`, [warehouseFk, beginningYear]);
`SELECT clh.dated, chn.name, cht.name, w.id
FROM vn.calendarHolidays clh
JOIN vn.workCenter w ON w.id = clh.workcenterFk
LEFT JOIN vn.calendarHolidaysName chn ON chn.id = clh.calendarHolidaysNameFk
LEFT JOIN vn.calendarHolidaysType cht ON cht.id = clh.calendarHolidaysTypeFk
WHERE w.warehouseFk = ? AND clh.dated >= ?`, [warehouseFk, beginningYear]);
return holidays.map(holiday => {
holiday.dated = new Date(holiday.dated);

View File

@ -5,7 +5,7 @@
"Department": {
"dataSource": "vn"
},
"Holiday": {
"CalendarHoliday": {
"dataSource": "vn"
},
"CalendarHolidaysName": {

View File

@ -1,17 +1,17 @@
{
"name": "Holiday",
"name": "CalendarHoliday",
"base": "VnModel",
"options": {
"mysql": {
"table": "holiday"
"table": "calendarHolidays"
}
},
"properties": {
"holidayDetailFk": {
"calendarHolidaysNameFk": {
"id": true,
"type": "Number"
},
"holidayTypeFk": {
"calendarHolidaysTypeFk": {
"id": true,
"type": "Number"
},

View File

@ -23,7 +23,7 @@
},
"holidays": {
"type": "hasMany",
"model": "Holiday",
"model": "CalendarHoliday",
"foreignKey": "workCenterFk"
}
},