#1315 item.ticketRequest
gitea/salix/dev This commit looks good Details

This commit is contained in:
Gerard 2019-04-05 15:20:12 +02:00
parent c9cd1b19dd
commit 633ab28c41
24 changed files with 760 additions and 34 deletions

View File

@ -12,16 +12,11 @@ export default class Controller extends Component {
element.addEventListener('focus', () => {
if (this.field || this.disabled) return;
$transclude((tClone, tScope) => {
this.field = tClone;
this.tScope = tScope;
this.element.querySelector('.field').appendChild(this.field[0]);
element.tabIndex = -1;
this.timer = $timeout(() => {
this.timer = null;
focus(this.field[0]);
});
}, null, 'field');
element.classList.add('selected');
});

View File

@ -19,6 +19,9 @@ vn-td-editable {
display: block
}
}
&[disabled="true"] {
cursor: not-allowed;
}
&.selected > .text {
visibility: hidden;
}

View File

@ -1,7 +1,7 @@
@import "variables";
vn-textfield {
margin: 20px 0!important;
margin: 20px 0;
display: inline-block;
width: 100%;
@ -145,3 +145,9 @@ vn-textfield {
background-color: #d50000;
}
}
vn-table {
vn-textfield {
margin: 0;
}
}

View File

@ -40,7 +40,6 @@ export default class Th {
this.order = 'DESC';
else
this.order = 'ASC';
}
/**
@ -71,7 +70,6 @@ export default class Th {
this.column.classList.add('desc');
else
this.column.classList.add('asc');
}
}

View File

@ -79,5 +79,6 @@
"EXTENSION_INVALID_FORMAT": "La extensión es invalida",
"We weren't able to send this SMS": "No hemos podido enviar el SMS",
"This client can't be invoiced": "Este cliente no puede ser facturado",
"This ticket can't be invoiced": "Este ticket no puede ser facturado"
"This ticket can't be invoiced": "Este ticket no puede ser facturado",
"That item is not available on that day": "That item is not available on that day"
}

View File

@ -5,7 +5,7 @@ const app = require('vn-loopback/server/server');
* Envio SMS de prueba a servicio real Masmovil. No llega a enviarse
* por destinatario inválido, pero puede llegar a fallar.
*/
fdescribe('sms send()', () => {
describe('sms send()', () => {
it('should call the send method', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let result = await app.models.Sms.send(ctx, null, 'Invalid', 'My SMS Body');

View File

@ -12,6 +12,8 @@ import './fetched-tags';
import './tags';
import './tax';
import './log';
import './request';
import './request-search-panel';
import './last-entries';
import './niche';
import './botanical';

View File

@ -9,11 +9,23 @@
<div class="content-block">
<div class="vn-list">
<vn-card pad-medium-h>
<vn-horizontal>
<vn-searchbar
vn-three
panel="vn-item-search-panel"
on-search="$ctrl.onSearch($params)"
vn-focus>
</vn-searchbar>
<vn-icon-menu
vn-id="more-button"
icon="more_vert"
show-filter="false"
value-field="callback"
translate-fields="['name']"
on-change="$ctrl.onMoreChange(value)"
on-open="$ctrl.onMoreOpen()">
</vn-icon-menu>
</vn-horizontal>
</vn-card>
</div>
<vn-card margin-medium-v>

View File

@ -2,7 +2,8 @@ import ngModule from '../module';
import './style.scss';
class Controller {
constructor($http, $state, $scope, $stateParams) {
constructor($http, $state, $scope, aclService) {
this.aclService = aclService;
this.$http = $http;
this.$state = $state;
this.$ = $scope;
@ -13,6 +14,22 @@ class Controller {
id: false,
actions: false
};
this.moreOptions = [
{callback: this.goToTicketRequest, name: 'Ticket request', acl: 'buyer'}
];
}
onMoreOpen() {
let options = this.moreOptions.filter(o => this.aclService.hasAny([o.acl]));
this.$.moreButton.data = options;
}
onMoreChange(callback) {
callback.call(this);
}
goToTicketRequest() {
this.$state.go('item.request');
}
stopEvent(event) {
@ -86,7 +103,7 @@ class Controller {
this.$.preview.show();
}
}
Controller.$inject = ['$http', '$state', '$scope', '$stateParams'];
Controller.$inject = ['$http', '$state', '$scope', 'aclService'];
ngModule.component('vnItemIndex', {
template: require('./index.html'),

View File

@ -1 +1,2 @@
picture: Foto
Ticket request: Peticiones de compra

View File

@ -1,5 +1,42 @@
@import "variables";
vn-item-index {
vn-icon-menu{
padding-top: 30px;
padding-left: 10px;
color: $color-main;
li {
color: initial;
}
}
}
vn-item-product {
display: block;
.id {
background-color: $color-main;
color: $color-font-dark;
margin-bottom: 0em;
}
.image {
height: 7em;
width: 7em;
& > img {
max-height: 100%;
max-width: 100%;
border-radius: .2em;
}
}
vn-label-value:first-of-type section{
margin-top: 0.6em;
}
vn-fetched-tags vn-horizontal{
margin-top: 0.9em;
}
}
vn-table {
img {
border-radius: 50%;

View File

@ -0,0 +1,57 @@
<div class="search-panel">
<form pad-large ng-submit="$ctrl.onSearch()">
<vn-horizontal>
<vn-textfield
vn-one
label="General search"
model="filter.search"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Ticket id"
model="filter.ticketFk">
</vn-textfield>
<vn-autocomplete
vn-one
field="filter.atenderFk"
url="/client/api/Clients/activeWorkersWithRole"
search-function="{firstName: $search}"
value-field="id"
where="{role: 'employee'}"
label="Atender">
<tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="Client id"
model="filter.clientFk">
</vn-textfield>
<vn-autocomplete
vn-one
label="Warehouse"
field="filter.warehouseFk"
url="/api/Warehouses">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="From"
model="filter.from">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
model="filter.to">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal margin-large-top>
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,7 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
ngModule.component('vnRequestSearchPanel', {
template: require('./index.html'),
controller: SearchPanel
});

View File

@ -0,0 +1,45 @@
import './index.js';
describe('Item', () => {
describe('Component vnItemSearchPanel', () => {
let $element;
let controller;
beforeEach(ngModule('item'));
beforeEach(angular.mock.inject($componentController => {
$element = angular.element(`<div></div>`);
controller = $componentController('vnItemSearchPanel', {$element});
}));
describe('getSourceTable()', () => {
it(`should return null if there's no selection`, () => {
let selection = null;
let result = controller.getSourceTable(selection);
expect(result).toBeNull();
});
it(`should return null if there's a selection but its isFree property is truthy`, () => {
let selection = {isFree: true};
let result = controller.getSourceTable(selection);
expect(result).toBeNull();
});
it(`should return the formated sourceTable concatenated to a path`, () => {
let selection = {sourceTable: 'hello guy'};
let result = controller.getSourceTable(selection);
expect(result).toEqual('/api/Hello guys');
});
it(`should return a path if there's no sourceTable and the selection has an id`, () => {
let selection = {id: 99};
let result = controller.getSourceTable(selection);
expect(result).toEqual(`/api/ItemTags/filterItemTags/${selection.id}`);
});
});
});
});

View File

@ -0,0 +1,3 @@
Ink: Tinta
Origin: Origen
Producer: Productor

View File

@ -0,0 +1,118 @@
<vn-crud-model
vn-id="model"
url="/api/TicketRequests/filter"
limit="20"
data="requests"
auto-load="false">
</vn-crud-model>
<form name="form">
<div margin-medium>
<vn-card pad-medium-h class="vn-list">
<vn-horizontal>
<vn-searchbar
panel="vn-request-search-panel"
on-search="$ctrl.onSearch($params)"
vn-one
vn-focus>
</vn-searchbar>
</vn-horizontal>
</vn-card>
<vn-card margin-medium-v pad-medium>
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th number field="ticketFk">Ticket ID</vn-th>
<vn-th field="shipped">Shipped</vn-th>
<vn-th field="warehouse">Warehouse</vn-th>
<vn-th field="salesPersonNickname">SalesPerson</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="quantity" editable>Quantity</vn-th>
<vn-th field="price">Price</vn-th>
<vn-th field="atenderNickname">Atender</vn-th>
<vn-th field="itemFk">itemFk</vn-th>
<vn-th field="description">Concept</vn-th>
<vn-th field="">Quantity</vn-th>
<vn-th>State</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="request in requests">
<vn-td number>
<span class="link"
ng-click="$ctrl.showTicketDescriptor($event, request.ticketFk)">
{{request.ticketFk}}
</span>
</vn-td>
<vn-td class="{{$ctrl.compareDate(request.shipped)}}">
{{::request.shipped | dateTime: 'dd/MM/yyyy'}}
</vn-td>
<vn-td>{{::request.warehouse}}</vn-td>
<vn-td>{{::request.salesPersonNickname}}</vn-td>
<vn-td>{{::request.description}}</vn-td>
<vn-td>{{::request.quantity}}</vn-td>
<vn-td>{{::request.price}}</vn-td>
<vn-td>{{::request.atenderNickname}}</vn-td>
<vn-td-editable>
<text>{{request.itemFk}}</text>
<field>
<vn-textfield
vn-focus
model="request.itemFk"
on-change="$ctrl.confirmRequest(request)"
type="number">
</vn-textfield>
</field>
</vn-td-editable>
<vn-td>{{::request.itemDescription}}</vn-td>
<vn-td-editable disabled="!request.saleFk && request.itemFk">
<text>{{request.saleQuantity}}</text>
<field>
<vn-textfield
vn-focus
model="request.saleQuantity"
on-change="$ctrl.changeQuantity(request)"
type="number">
</vn-textfield>
</field>
</vn-td-editable>
<vn-td>{{::$ctrl.getState(request.isOk)}}</vn-td>
<vn-td>
<vn-icon-button
disabled="request.isOk === 0"
number
icon="thumb_down"
ng-click="$ctrl.showDenyReason($event, request.id)"
vn-tooltip="Discard">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
<vn-pagination model="model"></vn-pagination>
</div>
</form>
<vn-client-descriptor-popover vn-id="clientDescriptor"></vn-client-descriptor-popover>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>
<vn-dialog
vn-id="denyReason"
class="modal-form">
<tpl-body>
<vn-horizontal class="header">
<h5><span translate>Indicate the reasons to deny this request</span></h5>
</vn-horizontal>
<vn-horizontal pad-medium>
<vn-textarea
model="$ctrl.denyObservation">
</vn-textarea>
</vn-horizontal>
<vn-horizontal pad-medium>
<vn-button
label="Save"
ng-click="$ctrl.denyRequest()">
</vn-button>
</vn-horizontal>
</tpl-body>
</vn-dialog>

View File

@ -0,0 +1,142 @@
import ngModule from '../module';
import './style.scss';
export default class Controller {
constructor($scope, vnApp, $translate, $http, $state, $stateParams) {
this.$state = $state;
this.$stateParams = $stateParams;
this.$http = $http;
this.$scope = $scope;
this.vnApp = vnApp;
this._ = $translate;
if (!$stateParams.q)
this.filter = {isOk: false, mine: true};
}
$postLink() {
if (this.filter)
this.onSearch(this.filter);
}
getState(isOk) {
if (isOk === null)
return 'Nueva';
else if (isOk === -1 || isOk === 1)
return 'Aceptada';
else
return 'Denegada';
}
confirmRequest(request) {
if (request.itemFk && request.quantity) {
let params = {
itemFk: request.itemFk,
quantity: request.quantity || request.saleQuantity
};
let endpoint = `/api/TicketRequests/${request.id}/confirm`;
this.$http.post(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
}).catch( e => {
this.$scope.model.refresh();
throw e;
});
}
}
changeQuantity(request) {
if (request.saleFk) {
let params = {
quantity: request.saleQuantity
};
let endpoint = `/api/Sales/${request.saleFk}/`;
this.$http.patch(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
}).catch( e => {
this.$scope.model.refresh();
throw e;
});
}
}
compareDate(date) {
let today = new Date();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0)
return 'warning';
if (comparation < 0)
return 'success';
}
onSearch(params) {
if (params)
this.$scope.model.applyFilter(null, params);
else
this.$scope.model.clear();
}
showDenyReason(event, requestId) {
this.denyRequestId = requestId;
this.$scope.denyReason.parent = event.target;
this.$scope.denyReason.show();
}
clear() {
delete this.denyRequestId;
}
denyRequest() {
let params = {
observation: this.denyObservation
};
let endpoint = `/api/TicketRequests/${this.denyRequestId}/deny`;
this.$http.post(endpoint, params).then(() => {
this.vnApp.showSuccess(this._.instant('Data saved!'));
this.$scope.model.refresh();
this.$scope.denyReason.hide();
this.denyObservation = null;
});
}
showClientDescriptor(event, clientFk) {
this.$scope.clientDescriptor.clientFk = clientFk;
this.$scope.clientDescriptor.parent = event.target;
this.$scope.clientDescriptor.show();
event.preventDefault();
event.stopImmediatePropagation();
}
showTicketDescriptor(event, ticketFk) {
this.$scope.ticketDescriptor.ticketFk = ticketFk;
this.$scope.ticketDescriptor.parent = event.target;
this.$scope.ticketDescriptor.show();
event.preventDefault();
event.stopImmediatePropagation();
}
onDescriptorLoad() {
this.$scope.popover.relocate();
}
preventNavigation(event) {
event.preventDefault();
event.stopImmediatePropagation();
}
}
Controller.$inject = ['$scope', 'vnApp', '$translate', '$http', '$state', '$stateParams'];
ngModule.component('vnItemRequest', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,2 @@
Discard: Descartar
Indicate the reasons to deny this request: Indique las razones para descartar esta peticion

View File

@ -0,0 +1,12 @@
vn-dialog[vn-id="denyReason"] {
button.close {
display: none
}
& vn-button {
margin: 0 auto
}
vn-textarea {
width: 100%
}
}

View File

@ -122,6 +122,15 @@
"state": "item.card.log",
"component": "vn-item-log",
"description": "Log"
}, {
"url" : "/request?q",
"state": "item.request",
"component": "vn-item-request",
"description": "Item request",
"params": {
"item": "$ctrl.item"
},
"acl": ["employee"]
}
]
}

View File

@ -0,0 +1,85 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('confirm', {
description: '',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Integer',
required: true,
description: 'The request ID',
}, {
arg: 'itemFk',
type: 'Integer',
required: true,
description: 'The request observation',
}, {
arg: 'quantity',
type: 'Integer',
required: true,
description: 'The request observation',
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/confirm`,
verb: 'post'
}
});
Self.confirm = async ctx => {
let transaction = await Self.beginTransaction({});
let options = {transaction: transaction};
try {
let item = await Self.app.models.Item.findById(ctx.args.itemFk);
if (!item)
throw new UserError(`That item doesn't exists`);
let request = await Self.app.models.TicketRequest.findById(ctx.args.id, {
include: {relation: 'ticket'}
});
let query = `CALL vn.getItemVisibleAvailable(?,?,?,?)`;
let params = [
ctx.args.itemFk,
request.ticket().warehouseFk,
request.ticket().shipped,
false
];
let [res] = await Self.rawSql(query, params);
let available = res[0].available;
if (!available)
throw new UserError(`That item is not available on that day`);
if (request.saleFk) {
let sale = await Self.app.models.Sale.findById(request.saleFk);
sale.updateAttributes({itemFk: ctx.args.itemFk, quantity: ctx.args.quantity, description: item.description}, options);
} else {
params = {
ticketFk: request.ticketFk,
itemFk: ctx.args.itemFk,
quantity: ctx.args.quantity
};
sale = await Self.app.models.Sale.create(params, options);
request.updateAttributes({saleFk: sale.id, itemFk: sale.itemFk}, options);
}
query = `CALL vn.ticketCalculateSale(?)`;
params = [sale.id];
await Self.rawSql(query, params, options);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
};
};

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('deny', {
description: 'Create a newticket and returns the new ID',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Integer',
required: true,
description: 'The request ID',
}, {
arg: 'observation',
type: 'String',
required: true,
description: 'The request observation',
}],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/deny`,
verb: 'post'
}
});
Self.deny = async ctx => {
let userId = ctx.req.accessToken.userId;
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}});
let params = {
isOk: false,
atenderFk: worker.id,
observation: ctx.args.observation,
};
let request = await Self.app.models.TicketRequest.findById(ctx.args.id);
return request.updateAttributes(params);
};
};

View File

@ -0,0 +1,131 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('filter', {
description: 'Find all instances of the model matched by filter from the data source.',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
}, {
arg: 'filter',
type: 'Object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}, {
arg: 'search',
type: 'String',
description: `If it's and integer searchs by id, otherwise it searchs by nickname`
}, {
arg: 'ticketFk',
type: 'Number',
description: `Searchs by ticketFk`
}, {
arg: 'warehouseFk',
type: 'Number',
description: `Search by warehouse`
}, {
arg: 'atenderFk',
type: 'Number',
description: `Search requests atended by the given worker`
}, {
arg: 'mine',
type: 'Boolean',
description: `Search requests attended by the connected worker`
}, {
arg: 'from',
type: 'Date',
description: `Date from`
}, {
arg: 'to',
type: 'Date',
description: `Date to`
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: '/filter',
verb: 'GET'
}
});
Self.filter = async(ctx, filter) => {
let conn = Self.dataSource.connector;
let userId = ctx.req.accessToken.userId;
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}});
if (ctx.args.mine)
ctx.args.atenderFk = worker.id;
let where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.ticketFk': {inq: value}}
: {'t.nickname': {like: `%${value}%`}};
case 'ticketFk':
return {'t.id': value};
case 'atenderFk':
return {'tr.atenderFk': value};
case 'isOk':
return {'tr.isOk': value};
case 'clientFk':
return {'t.clientFk': value};
case 'from':
return {'t.shipped': {gte: value}};
case 'to':
return {'t.shipped': {lte: value}};
case 'warehouse':
return {'w.id': value};
case 'salesPersonFk':
return {'c.salesPersonFk': value};
}
});
filter = mergeFilters(filter, {where});
let stmt;
stmt = new ParameterizedSQL(
`SELECT
tr.id,
tr.ticketFk,
tr.quantity,
tr.price,
tr.atenderFk,
tr.description,
tr.itemFk,
tr.saleFk,
tr.isOk,
s.quantity AS saleQuantity,
i.description AS itemDescription,
t.shipped,
t.nickname,
t.warehouseFk,
t.clientFk,
w.name AS warehouse,
u.nickname AS salesPersonNickname,
ua.nickname AS atenderNickname
FROM ticketRequest tr
LEFT JOIN ticket t ON t.id = tr.ticketFk
LEFT JOIN warehouse w ON w.id = t.warehouseFk
LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN item i ON i.id = tr.itemFk
LEFT JOIN sale s ON s.id = tr.saleFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.userFk
LEFT JOIN worker wka ON wka.id = tr.atenderFk
LEFT JOIN account.user ua ON ua.id = wka.userFk`);
stmt.merge(conn.makeSuffix(filter));
let result = await conn.executeStmt(stmt);
return result;
};
};

View File

@ -1,6 +1,10 @@
const LoopBackContext = require('loopback-context');
module.exports = function(Self) {
require('../methods/ticket-request/filter')(Self);
require('../methods/ticket-request/deny')(Self);
require('../methods/ticket-request/confirm')(Self);
Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) {
const loopBackContext = LoopBackContext.getCurrentContext();