refs #3963 test front done
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alexandre Riera 2022-11-14 14:30:35 +01:00
parent 99aa674995
commit 23ed6c6031
15 changed files with 790 additions and 8 deletions

View File

@ -0,0 +1,97 @@
DROP PROCEDURE IF EXISTS vn.ticket_canAdvance;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDated DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar.
*
* @param vDated Fecha de los tickets que se quieren adelantar.
* @param vWarehouseFk Almacén
*/
DECLARE vDateInventory DATE;
DECLARE vDateToAdvance DATE;
SELECT inventoried INTO vDateInventory FROM vn.config;
SET vDateToAdvance = TIMESTAMPADD(DAY,-1,vDated);
DROP TEMPORARY TABLE IF EXISTS tmp.stock;
CREATE TEMPORARY TABLE tmp.stock
(itemFk INT PRIMARY KEY,
amount INT)
ENGINE = MEMORY;
INSERT INTO tmp.stock(itemFk, amount)
SELECT itemFk, SUM(quantity) amount FROM
(
SELECT itemFk, quantity
FROM vn.itemTicketOut
WHERE shipped >= vDateInventory
AND shipped < vDated
AND warehouseFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM vn.itemEntryIn
WHERE landed >= vDateInventory
AND landed < vDated
AND isVirtualStock = FALSE
AND warehouseInFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM vn.itemEntryOut
WHERE shipped >= vDateInventory
AND shipped < vDated
AND warehouseOutFk = vWarehouseFk
) t
GROUP BY itemFk HAVING amount != 0;
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT s.ticketFk ticketFuture,
sum((s.quantity <= IFNULL(st.amount,0))) hasStock,
count(DISTINCT s.id) saleCount,
st.name tfState,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) tfIpt,
t2.ticketFk id,
t2.state,
t2.ipt,
t.workerFk,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
t2.shipped,
t.shipped tfShipped,
t2.totalWithVat,
t.totalWithVat tfTotalWithVat
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.saleVolume sv ON t.id = sv.ticketFk
LEFT JOIN (SELECT
t2.id ticketFk,
t2.addressFk,
st.name state,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
t2.shipped,
t2.totalWithVat
FROM vn.ticket t2
JOIN vn.sale s ON s.ticketFk = t2.id
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticketState ts ON ts.ticketFk = t2.id
JOIN vn.state st ON st.id = ts.stateFk
WHERE t2.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance)
AND t2.warehouseFk = vWarehouseFk
GROUP BY t2.id) t2 ON t2.addressFk = t.addressFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
LEFT JOIN tmp.stock st ON st.itemFk = s.itemFk
WHERE t.shipped BETWEEN vDated AND util.dayend(vDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id;
DROP TEMPORARY TABLE tmp.stock;
END$$
DELIMITER ;

View File

@ -0,0 +1,2 @@
DROP PROCEDURE IF EXISTS `ticket_split`;
DROP PROCEDURE IF EXISTS `ticket_merge`;

View File

@ -0,0 +1,142 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('getTicketsAdvance', {
description: 'Find all tickets that can be moved to the present',
accessType: 'READ',
accepts: [
{
arg: 'vDated',
type: 'date',
description: 'The date in question',
required: false
},
{
arg: 'warehouseFk',
type: 'number',
description: 'Warehouse identifier',
required: false
},
{
arg: 'shipped',
type: 'date',
description: 'Origin shipped',
required: false
},
{
arg: 'tfShipped',
type: 'date',
description: 'Destination shipped',
required: false
},
{
arg: 'ipt',
type: 'string',
description: 'Origin Item Packaging Type',
required: false
},
{
arg: 'tfIpt',
type: 'string',
description: 'Destination Item Packaging Type',
required: false
},
{
arg: 'id',
type: 'number',
description: 'Origin id',
required: false
},
{
arg: 'tfId',
type: 'number',
description: 'Destination id',
required: false
},
{
arg: 'state',
type: 'string',
description: 'Origin state',
required: false
},
{
arg: 'tfState',
type: 'string',
description: 'Destination state',
required: false
},
{
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/getTicketsAdvance`,
verb: 'GET'
}
});
Self.getTicketsAdvance = async (ctx, options) => {
const args = ctx.args;
console.log(args);
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'shipped':
return { 'f.shipped': value };
case 'tfShipped':
return { 'f.tfShipped': value };
case 'ipt':
return { 'f.ipt': value };
case 'tfIpt':
return { 'f.tfIpt': value };
case 'state':
return { 'f.state': { like: `%${value}%` } };
case 'tfState':
return { 'f.tfState': { like: `%${value}%` } };
}
});
let filter = mergeFilters(ctx.args.filter, { where });
const stmts = [];
let stmt;
stmt = new ParameterizedSQL(
`CALL vn.ticket_canAdvance(?,?)`,
[new Date(), args.warehouseFk]);
stmts.push(stmt);
stmt = new ParameterizedSQL(`
SELECT f.*
FROM tmp.filter f`);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makeOrderBy(filter.order));
stmt.merge(conn.makeLimit(filter));
const ticketsIndex = stmts.push(stmt) - 1;
stmts.push(
`DROP TEMPORARY TABLE
tmp.filter`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return result[ticketsIndex];
};
};

View File

@ -41,7 +41,7 @@
"SaleTracking": {
"dataSource": "vn"
},
"State":{
"State": {
"dataSource": "vn"
},
"Ticket": {
@ -68,16 +68,16 @@
"TicketRequest": {
"dataSource": "vn"
},
"TicketState":{
"TicketState": {
"dataSource": "vn"
},
"TicketLastState":{
"TicketLastState": {
"dataSource": "vn"
},
"TicketService":{
"TicketService": {
"dataSource": "vn"
},
"TicketServiceType":{
"TicketServiceType": {
"dataSource": "vn"
},
"TicketTracking": {
@ -94,8 +94,7 @@
},
"TicketFuture": {
"dataSource": "vn"
}
,
},
"TicketAdvance": {
"dataSource": "vn"
}

View File

@ -0,0 +1,12 @@
{
"name": "TicketAdvance",
"base": "PersistedModel",
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -0,0 +1,74 @@
<div class="search-panel">
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-date-picker
vn-one
label="Origin date"
ng-model="filter.shipped">
</vn-date-picker>
<vn-date-picker
vn-one
label="Destination date"
ng-model="filter.tfShipped">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
data="$ctrl.itemPackingTypes"
label="Origin IPT"
value-field="code"
show-field="name"
ng-model="filter.ipt"
info="IPT">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one
data="$ctrl.itemPackingTypes"
label="Destination IPT"
value-field="code"
show-field="name"
ng-model="filter.tfIpt"
info="IPT">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
data="$ctrl.groupedStates"
label="Origin Grouped State"
value-field="name"
show-field="name"
ng-model="filter.state">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one
data="$ctrl.groupedStates"
label="Destination Grouped State"
value-field="name"
show-field="name"
ng-model="filter.tfState">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="filter.warehouseFk"
url="Warehouses"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -0,0 +1,44 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
this.getGroupedStates();
this.getItemPackingTypes();
}
getGroupedStates() {
let groupedStates = [];
this.$http.get('AlertLevels').then(res => {
for (let state of res.data) {
groupedStates.push({
id: state.id,
code: state.code,
name: this.$t(state.code)
});
}
this.groupedStates = groupedStates;
});
}
getItemPackingTypes() {
let itemPackingTypes = [];
this.$http.get('ItemPackingTypes').then(res => {
for (let ipt of res.data) {
itemPackingTypes.push({
id: ipt.id,
code: ipt.code,
name: this.$t(ipt.code)
});
}
this.itemPackingTypes = itemPackingTypes;
});
}
}
ngModule.vnComponent('vnAdvanceTicketSearchPanel', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1 @@
Advance tickets: Advance tickets

View File

@ -0,0 +1 @@
Advance tickets: Adelantar tickets

View File

@ -0,0 +1,154 @@
<vn-crud-model
vn-id="model"
url="Tickets/getTicketsAdvance"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-advance-ticket-search-panel"
placeholder="Search tickets"
info="Search advance tickets by date"
auto-state="false"
model="model">
</vn-searchbar>
</vn-portal>
<vn-card>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"
>
<slot-actions>
<vn-button disabled="$ctrl.checked.length === 0"
icon="keyboard_double_arrow_left"
ng-click="moveTicketsAdvance.show($event)"
vn-tooltip="Advance tickets">
</vn-button>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th shrink>
<vn-multi-check
model="model"
checked="$ctrl.checkAll"
check-field="checked">
</vn-multi-check>
</th>
<th field="totalWithVat">
<span translate>Import</span>
</th>
<th field="ticketFk">
<span translate>Origin ID</span>
</th>
<th field="state">
<span translate>Origin State</span>
</th>
<th field="ipt">
<span>IPT</span>
</th>
<th field="ticketFuture">
<span translate>Destination ID</span>
</th>
<th field="tfState">
<span translate>Destination State</span>
</th>
<th field="liters">
<span translate>Liters</span>
</th>
<th field="lines">
<span translate>Lines</span>
</th>
<!-- <th field="destETD">
<span translate>Destination ETD</span>
</th>
<th field="stock">
<span translate>Stock</span>
</th> -->
<th field="hasStock">
<span>Stock</span>
</th>
<th field="tfIpt">
<span>IPT</span>
</th>
<th field="tfTotalWithVat">
<span translate>Import</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ticket in model.data">
<td>
<vn-check
ng-model="ticket.checked"
vn-click-stop>
</vn-check>
</td>
<td>
<span class="chip {{$ctrl.totalPriceColor(ticket.totalWithVat)}}">
{{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
</span>
</td>
<td>
<span
ng-click="ticketDescriptor.show($event, ticket.id)"
class="link">
{{::ticket.id | dashIfEmpty}}
</span>
</td>
<!-- <td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.originETD)}}">
{{::ticket.originETD | date: 'dd/MM/yyyy'}}
</span>
</td> -->
<td>
<span
class="chip {{$ctrl.stateColor(ticket.state)}}">
{{::ticket.state | dashIfEmpty}}
</span>
</td>
<td>{{::ticket.ipt | dashIfEmpty}}</td>
<td>
<span
ng-click="ticketDescriptor.show($event, ticket.ticketFuture)"
class="link">
{{::ticket.ticketFuture | dashIfEmpty}}
</span>
</td>
<td>
<span
class="chip {{$ctrl.stateColor(ticket.tfState)}}">
{{::ticket.tfState | dashIfEmpty}}
</span>
</td>
<!-- <td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.destETD)}}">
{{::ticket.destETD | date: 'dd/MM/yyyy'}}
</span>
</td> -->
<td>{{::ticket.liters | dashIfEmpty}}</td>
<td>{{::ticket.lines | dashIfEmpty}}</td>
<td>{{::ticket.hasStock | dashIfEmpty}}</td>
<td>{{::ticket.tfIpt | dashIfEmpty}}</td>
<td>
<span class="chip {{$ctrl.totalPriceColor(ticket.tfTotalWithVat)}}">
{{::(ticket.tfTotalWithVat ? ticket.tfTotalWithVat : 0) | currency: 'EUR': 2}}
</span>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-confirm
vn-id="moveTicketsAdvance"
on-accept="$ctrl.moveTicketsAdvance()"
question="{{$ctrl.confirmationMessage}}"
message="Advance tickets">
</vn-confirm>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>

View File

@ -0,0 +1,135 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.$checkAll = false;
this.smartTableOptions = {
activeButtons: {
search: true,
},
columns: [
{
field: 'state',
searchable: false
},
{
field: 'tfState',
searchable: false
},
{
field: 'ipt',
autocomplete: {
url: 'ItemPackingTypes',
showField: 'description',
valueField: 'code'
}
},
{
field: 'tfIpt',
autocomplete: {
url: 'ItemPackingTypes',
showField: 'description',
valueField: 'code'
}
},
]
};
}
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';
}
get checked() {
const tickets = this.$.model.data || [];
const checkedLines = [];
for (let ticket of tickets) {
if (ticket.checked)
checkedLines.push(ticket);
}
return checkedLines;
}
stateColor(state) {
if (state === 'OK')
return 'success';
else if (state === 'Libre')
return 'notice';
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
totalPriceColor(totalWithVat) {
const total = parseInt(totalWithVat);
if (total > 0 && total < 50)
return 'warning';
}
get confirmationMessage() {
if (!this.$.model) return 0;
return this.$t(`Advance confirmation`, {
checked: this.checked.length
});
}
moveTicketsAdvance() {
let params = { tickets: this.checked };
return this.$http.post('Tickets/merge', params)
.then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Success'));
});
}
exprBuilder(param, value) {
switch (param) {
case 'id':
return { 'id': value };
case 'ticketFuture':
return { 'ticketFuture': value };
case 'liters':
return { 'liters': value };
case 'lines':
return { 'lines': value };
case 'ipt':
return { 'ipt': value };
case 'tfIpt':
return { 'tfIpt': value };
case 'totalWithVat':
return { 'totalWithVat': value };
case 'tfTotalWithVat':
return { 'tfTotalWithVat': value };
case 'hasStock':
return { 'hasStock': value };
}
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnTicketAdvance', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,113 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Component vnTicketAdvance', () => {
let controller;
let $httpBackend;
let $window;
beforeEach(ngModule('ticket')
);
beforeEach(inject(($componentController, _$window_, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$window = _$window_;
const $element = angular.element('<vn-ticket-advance></vn-ticket-advance>');
controller = $componentController('vnTicketAdvance', { $element });
controller.$.model = crudModel;
controller.$.model.data = [{
id: 1,
checked: true,
state: "OK"
}, {
id: 2,
checked: true,
state: "Libre"
}];
}));
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let today = new Date();
let result = controller.compareDate(today);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = new Date();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = new Date();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('checked()', () => {
it('should return an array of checked tickets', () => {
const result = controller.checked;
const firstRow = result[0];
const secondRow = result[1];
expect(result.length).toEqual(2);
expect(firstRow.id).toEqual(1);
expect(secondRow.id).toEqual(2);
});
});
describe('stateColor()', () => {
it('should return success to the OK tickets', () => {
const ok = controller.stateColor(controller.$.model.data[0].state);
const notOk = controller.stateColor(controller.$.model.data[1].state);
expect(ok).toEqual('success');
expect(notOk).not.toEqual('success');
});
it('should return success to the FREE tickets', () => {
const notFree = controller.stateColor(controller.$.model.data[0].state);
const free = controller.stateColor(controller.$.model.data[1].state);
expect(free).toEqual('notice');
expect(notFree).not.toEqual('notice');
});
});
describe('dateRange()', () => {
it('should return two dates with the hours at the start and end of the given date', () => {
const now = new Date();
const today = now.getDate();
const dateRange = controller.dateRange(now);
const start = dateRange[0].toString();
const end = dateRange[1].toString();
expect(start).toContain(today);
expect(start).toContain('00:00:00');
expect(end).toContain(today);
expect(end).toContain('23:59:59');
});
});
describe('moveTicketsAdvance()', () => {
it('should make an HTTP Post query', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`Tickets/merge`).respond();
controller.moveTicketsAdvance();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
});

View File

@ -0,0 +1,2 @@
Advance tickets: Advance tickets
Success: Tickets moved successfully!

View File

@ -0,0 +1,6 @@
Advance tickets: Adelantar tickets
Search advance tickets by date: Busca tickets para adelantar por fecha
Advance confirmation: ¿Desea adelantar {{checked}} tickets?
Success: Tickets movidos correctamente
Lines: Líneas
Liters: Litros

View File

@ -8,7 +8,7 @@
"main": [
{"state": "ticket.index", "icon": "icon-ticket"},
{"state": "ticket.weekly.index", "icon": "schedule"},
{"state": "ticket.future", "icon": "double_arrow"},
{"state": "ticket.future", "icon": "keyboard_double_arrow_right"},
{"state": "ticket.advance", "icon": "keyboard_double_arrow_left"}
],
"card": [