changes to percentage filter
gitea/salix/master This commit looks good Details

This commit is contained in:
Joan Sanchez 2020-01-16 13:39:32 +01:00
parent 3a4c379033
commit 826dfe8092
19 changed files with 174 additions and 322 deletions

View File

@ -1,30 +0,0 @@
USE `bs`;
DROP procedure IF EXISTS `waste_getDetail`;
DELIMITER $$
USE `bs`$$
CREATE DEFINER=`root`@`%`PROCEDURE `waste_getDetail` ()
BEGIN
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = CURDATE();
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE `year` = vYear AND `week` = vWeek
GROUP BY buyer, family
) sub
ORDER BY percentage DESC;
END$$
DELIMITER ;

View File

@ -1119,6 +1119,19 @@ INSERT INTO `bi`.`claims_ratio`(`id_Cliente`, `Consumo`, `Reclamaciones`, `Ratio
(103, 2000, 0.00, 0.00, 0.02, 1.00), (103, 2000, 0.00, 0.00, 0.02, 1.00),
(104, 2500, 150.00, 0.02, 0.10, 1.00); (104, 2500, 150.00, 0.02, 0.10, 1.00);
INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `saleTotal`, `saleWaste`, `rate`)
VALUES
('CharlesXavier', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Clavel', '1062', '51', '4.8'),
('CharlesXavier', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Clavel Colombia', '35074', '687', '2.0'),
('CharlesXavier', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Clavel Mini', '1777', '13', '0.7'),
('CharlesXavier', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Clavel Short', '9182', '59', '0.6'),
('DavidCharlesHaller', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Contenedores', '-74', '0', '0.0'),
('DavidCharlesHaller', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Embalajes', '-7', '0', '0.0'),
('DavidCharlesHaller', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Portes', '1100', '0', '0.0'),
('HankPym', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Accesorios Funerarios', '848', '-187', '-22.1'),
('HankPym', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Accesorios Varios', '186', '0', '0.0'),
('HankPym', YEAR(CURDATE()), WEEK(CURDATE(), 1), 'Adhesivos', '277', '0', '0.0');
INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`minPrice`,`producer`,`printedStickers`,`isChecked`,`isIgnored`, `created`) INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`minPrice`,`producer`,`printedStickers`,`isChecked`,`isIgnored`, `created`)
VALUES VALUES
(1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, DATE_ADD(CURDATE(), INTERVAL -2 MONTH)), (1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, DATE_ADD(CURDATE(), INTERVAL -2 MONTH)),

View File

@ -29,7 +29,7 @@ describe('Ticket List sale path', () => {
const value = await nightmare const value = await nightmare
.waitToGetProperty(selectors.ticketSales.firstSaleDiscount, 'innerText'); .waitToGetProperty(selectors.ticketSales.firstSaleDiscount, 'innerText');
expect(value).toContain('0 %'); expect(value).toContain('0.00%');
}); });
it('should confirm the first sale contains the total import', async() => { it('should confirm the first sale contains the total import', async() => {

View File

@ -173,10 +173,10 @@ xdescribe('Ticket Edit sale path', () => {
it('should confirm the discount have been updated', async() => { it('should confirm the discount have been updated', async() => {
const result = await nightmare const result = await nightmare
.waitForTextInElement(`${selectors.ticketSales.firstSaleDiscount} > span`, '50 %') .waitForTextInElement(`${selectors.ticketSales.firstSaleDiscount} > span`, '50.00%')
.waitToGetProperty(`${selectors.ticketSales.firstSaleDiscount} > span`, 'innerText'); .waitToGetProperty(`${selectors.ticketSales.firstSaleDiscount} > span`, 'innerText');
expect(result).toContain('50 %'); expect(result).toContain('50.00%');
}); });
it('should confirm the total import for that item have been updated', async() => { it('should confirm the total import for that item have been updated', async() => {

View File

@ -1,15 +1,22 @@
import ngModule from '../module'; import ngModule from '../module';
/** export default function percentage($translate) {
* Formats a number multiplying by 100 and adding character %. function percentage(input, fractionSize = 2) {
*
* @return {String} The formated number
*/
export default function percentage() {
return function(input) {
if (input == null || input === '') if (input == null || input === '')
return null; return null;
return `${input} %`;
}; return new Intl.NumberFormat($translate.use(), {
style: 'percent',
minimumFractionDigits: fractionSize,
maximumFractionDigits: fractionSize
}).format(parseFloat(input));
}
percentage.$stateful = true;
return percentage;
} }
percentage.$inject = ['$translate'];
ngModule.filter('percentage', percentage); ngModule.filter('percentage', percentage);

View File

@ -0,0 +1,52 @@
module.exports = Self => {
Self.remoteMethod('getWasteDetail', {
description: 'Returns the ',
accessType: 'READ',
accepts: [],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/getWasteDetail`,
verb: 'GET'
}
});
Self.getWasteDetail = async() => {
const wastes = await Self.rawSql(`
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE year = YEAR(CURDATE()) AND week = WEEK(CURDATE(), 1)
GROUP BY buyer, family
) sub
ORDER BY percentage DESC;`);
const details = [];
for (let waste of wastes) {
const buyerName = waste.buyer;
let buyerDetail = details.find(waste => {
return waste.buyer == buyerName;
});
if (!buyerDetail) {
buyerDetail = {
buyer: buyerName,
lines: []
};
details.push(buyerDetail);
}
buyerDetail.lines.push(waste);
}
return details;
};
};

View File

@ -0,0 +1,23 @@
const app = require('vn-loopback/server/server');
describe('item getWasteDetail()', () => {
it('should check for the waste breakdown for every worker', async() => {
let result = await app.models.Item.getWasteDetail();
const firstBuyer = result[0].buyer;
const firstBuyerLines = result[0].lines;
const secondBuyer = result[1].buyer;
const secondBuyerLines = result[1].lines;
const thirdBuyer = result[2].buyer;
const thirdBuyerLines = result[2].lines;
expect(result.length).toEqual(3);
expect(firstBuyer).toEqual('CharlesXavier');
expect(firstBuyerLines.length).toEqual(4);
expect(secondBuyer).toEqual('DavidCharlesHaller');
expect(secondBuyerLines.length).toEqual(3);
expect(thirdBuyer).toEqual('HankPym');
expect(thirdBuyerLines.length).toEqual(3);
});
});

View File

@ -11,6 +11,7 @@ module.exports = Self => {
require('../methods/item/regularize')(Self); require('../methods/item/regularize')(Self);
require('../methods/item/getVisibleAvailable')(Self); require('../methods/item/getVisibleAvailable')(Self);
require('../methods/item/new')(Self); require('../methods/item/new')(Self);
require('../methods/item/getWasteDetail')(Self);
Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'}); Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'});

View File

@ -60,4 +60,5 @@ Barcodes: Códigos de barras
Diary: Histórico Diary: Histórico
Item diary: Registro de compra-venta Item diary: Registro de compra-venta
Last entries: Últimas entradas Last entries: Últimas entradas
Tags: Etiquetas Tags: Etiquetas
Waste breakdown: Desglose de mermas

View File

@ -7,7 +7,8 @@
"menus": { "menus": {
"main": [ "main": [
{"state": "item.index", "icon": "icon-item"}, {"state": "item.index", "icon": "icon-item"},
{"state": "item.request", "icon": "pan_tool"} {"state": "item.request", "icon": "pan_tool"},
{"state": "item.waste", "icon": "icon-claims"}
], ],
"card": [ "card": [
{"state": "item.card.basicData", "icon": "settings"}, {"state": "item.card.basicData", "icon": "settings"},
@ -141,11 +142,8 @@
"url" : "/waste", "url" : "/waste",
"state": "item.waste", "state": "item.waste",
"component": "vn-item-waste", "component": "vn-item-waste",
"description": "Waste", "description": "Waste breakdown",
"params": { "acl": ["buyer"]
"item": "$ctrl.item"
},
"acl": ["employee"]
} }
] ]
} }

View File

@ -1,120 +1,32 @@
<vn-crud-model auto-load="true" <vn-crud-model auto-load="true"
vn-id="model" vn-id="model"
url="Items/filter" url="Items/getWasteDetail"
limit="20" data="details">
data="requests"
order="shipped DESC, isOk ASC">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model"> <vn-data-viewer model="model">
<vn-card> <vn-card>
<vn-table model="model"> <section ng-repeat="detail in details" class="vn-pa-md">
<vn-thead> <vn-horizontal class="header">
<vn-tr> <h5><span translate>{{detail.buyer}}</span></h5>
<vn-th field="buer" number>Buyer</vn-th> </vn-horizontal>
<vn-th field="family">Family</vn-th> <vn-table>
<vn-th field="percentage">Percentage</vn-th> <vn-thead>
<vn-th field="salesPersonNickname">Mermas</vn-th> <vn-tr>
<vn-th field="total">Total</vn-th> <vn-th>Family</vn-th>
</vn-tr> <vn-th shrink>Percentage</vn-th>
</vn-thead> <vn-th number>Dwindle</vn-th>
<vn-tbody> <vn-th number>Total</vn-th>
<vn-tr ng-repeat="request in requests"> </vn-tr>
<vn-td number> </vn-thead>
<span class="link" <vn-tbody>
ng-click="$ctrl.showTicketDescriptor($event, request.ticketFk)"> <vn-tr ng-repeat="waste in detail.lines">
{{request.ticketFk}} <vn-td>{{::waste.family}}</vn-td>
</span> <vn-td shrink>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
</vn-td> <vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>
<vn-td> <vn-td number>{{::waste.total | currency: 'EUR'}}</vn-td>
<span title="{{::request.shipped | date: 'dd/MM/yyyy'}}" </vn-tr>
class="chip {{$ctrl.compareDate(request.shipped)}}"> </vn-tbody>
{{::request.shipped | date: 'dd/MM/yyyy'}} </vn-table>
</span> </section>
</vn-td>
<vn-td>{{::request.warehouse}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.salesPersonFk)">
{{::request.salesPersonNickname}}
</span>
</vn-td>
<vn-td title="{{::request.description}}">{{::request.description}}</vn-td>
<vn-td number>{{::request.quantity}}</vn-td>
<vn-td number>{{::request.price | currency: 'EUR':2}}</vn-td>
<vn-td>
<span
class="link"
ng-click="$ctrl.showWorkerDescriptor($event, request.attenderFk)">
{{::request.atenderNickname}}
</span>
</vn-td>
<vn-td-editable disabled="request.isOk != null" number>
<text>{{request.itemFk}}</text>
<field>
<vn-input-number class="dense" vn-focus
ng-model="request.itemFk">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable disabled="!request.itemFk || request.isOk != null" number>
<text number>{{request.saleQuantity}}</text>
<field>
<vn-input-number class="dense" vn-focus
ng-model="request.saleQuantity"
on-change="$ctrl.changeQuantity(request)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td>
<span
class="link"
ng-click="$ctrl.showItemDescriptor($event, request.itemFk)"
title="{{request.itemDescription}}">
{{request.itemDescription}}
</span>
</vn-td>
<vn-td>{{$ctrl.getState(request.isOk)}}</vn-td>
<vn-td>
<vn-icon
ng-if="request.response.length"
ranslate-attr="{title: request.response}"
icon="insert_drive_file">
</vn-icon>
<vn-icon-button
ng-if="request.isOk != 0"
icon="thumb_down"
ng-click="$ctrl.showDenyReason($event, request)"
translate-attr="{title: 'Discard'}">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>
<vn-item-descriptor-popover
vn-id="itemDescriptor">
</vn-item-descriptor-popover>
<vn-dialog
vn-id="denyReason"
on-response="$ctrl.denyRequest($response)">
<tpl-body>
<h5 class="vn-pa-md" translate>Specify the reasons to deny this request</h5>
<vn-horizontal class="vn-pa-md">
<vn-textarea
ng-model="$ctrl.denyObservation">
</vn-textarea>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -2,11 +2,7 @@ import ngModule from '../module';
import Component from 'core/lib/component'; import Component from 'core/lib/component';
import './style.scss'; import './style.scss';
export default class Controller extends Component {
}
ngModule.component('vnItemWaste', { ngModule.component('vnItemWaste', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller controller: Component
}); });

View File

@ -1,133 +0,0 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Item', () => {
describe('Component vnItemRequest', () => {
let $scope;
let $element;
let controller;
let $httpBackend;
beforeEach(ngModule('item'));
beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$scope = $rootScope.$new();
$scope.model = crudModel;
$scope.denyReason = {hide: () => {}};
$element = angular.element('<vn-item-request></vn-item-request>');
controller = $componentController('vnItemRequest', {$element, $scope});
}));
afterAll(() => {
$scope.$destroy();
$element.remove();
});
describe('getState()', () => {
it(`should return an string depending to the isOK value`, () => {
let isOk = null;
let result = controller.getState(isOk);
expect(result).toEqual('Nueva');
isOk = 1;
result = controller.getState(isOk);
expect(result).toEqual('Aceptada');
isOk = 0;
result = controller.getState(isOk);
expect(result).toEqual('Denegada');
});
});
describe('confirmRequest()', () => {
it(`should do nothing if the request does't have itemFk or saleQuantity`, () => {
let request = {};
spyOn(controller.vnApp, 'showSuccess');
controller.confirmRequest(request);
expect(controller.vnApp.showSuccess).not.toHaveBeenCalledWith();
});
it('should perform a query and call vnApp.showSuccess() and refresh if the conditions are met', () => {
spyOn(controller.vnApp, 'showSuccess');
let model = controller.$.model;
spyOn(model, 'refresh');
const expectedResult = {concept: 'Melee Weapon'};
let request = {itemFk: 1, saleQuantity: 1, id: 1};
$httpBackend.when('POST', `TicketRequests/${request.id}/confirm`).respond(expectedResult);
$httpBackend.expect('POST', `TicketRequests/${request.id}/confirm`).respond(expectedResult);
controller.confirmRequest(request);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
});
});
describe('changeQuantity()', () => {
it(`should call confirmRequest() if there's no sale id in the request`, () => {
let request = {};
spyOn(controller, 'confirmRequest');
controller.changeQuantity(request);
expect(controller.confirmRequest).toHaveBeenCalledWith(jasmine.any(Object));
});
it(`should perform a query and call vnApp.showSuccess() if the conditions are met`, () => {
let request = {saleFk: 1, saleQuantity: 1};
spyOn(controller.vnApp, 'showSuccess');
$httpBackend.when('PATCH', `Sales/${request.saleFk}/`).respond();
$httpBackend.expect('PATCH', `Sales/${request.saleFk}/`).respond();
controller.changeQuantity(request);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
});
});
describe('compareDate()', () => {
it(`should return "success" if receives a future date`, () => {
let date = '3019-02-18T11:00:00.000Z';
let result = controller.compareDate(date);
expect(result).toEqual('success');
});
it(`should return "warning" if date is today`, () => {
let date = new Date();
let result = controller.compareDate(date);
expect(result).toEqual('warning');
});
});
describe('denyRequest()', () => {
it(`should perform a query and call vnApp.showSuccess(), refresh(), hide() and set denyObservation to null in the controller`, () => {
spyOn(controller.vnApp, 'showSuccess');
const request = {id: 1};
const expectedResult = {isOk: false, attenderFk: 106, response: 'Denied!'};
controller.selectedRequest = request;
$httpBackend.when('POST', `TicketRequests/${request.id}/deny`).respond(expectedResult);
$httpBackend.expect('POST', `TicketRequests/${request.id}/deny`).respond(expectedResult);
controller.denyRequest('accept');
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
});
});
});
});

View File

@ -1,6 +1,3 @@
Discard: Descartar Family: Familia
Specify the reasons to deny this request: Especifica las razones para descartar la petición Percentage: Porcentaje
Buy requests: Peticiones de compra Dwindle: Mermas
Search request by id or alias: Buscar peticiones por identificador o alias
Requested: Solicitado
Achieved: Conseguido

View File

@ -1,18 +1,19 @@
@import "variables"; @import "variables";
vn-item-request { vn-item-waste {
vn-dialog[vn-id="denyReason"] { .header {
button.close { margin-bottom: 16px;
display: none text-transform: uppercase;
} font-size: 15pt;
vn-button { line-height: 1;
margin: 0 auto padding: 7px;
} padding-bottom: 7px;
vn-textarea { padding-bottom: 4px;
width: 100% font-weight: lighter;
} background-color: #fde6ca;
} border-bottom: 0.1em solid #f7931e;
vn-icon[icon=insert_drive_file]{ white-space: nowrap;
color: $color-font-secondary; overflow: hidden;
text-overflow: ellipsis;
} }
} }

View File

@ -4,5 +4,6 @@ module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`, `${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`, `${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`, `${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`]) `${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles(); .mergeStyles();

View File

@ -0,0 +1,5 @@
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -49,8 +49,15 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p v-html="$t('wasteDetailLink')"></p>
<div class="external-link vn-pa-sm vn-m-md">
<a href="https://salix.verdnatura.es/#!/item/waste" target="_blank">
https://salix.verdnatura.es/#!/item/waste
</a>
</div>
</div> </div>
</div> </div>
<!-- Footer block --> <!-- Footer block -->
<div class="grid-row"> <div class="grid-row">
<div class="grid-block"> <div class="grid-block">

View File

@ -5,4 +5,5 @@ description: A continuación se muestra la merma semanal a fecha de <strong>{0}<
buyer: Comprador buyer: Comprador
percentage: Porcentaje percentage: Porcentaje
weakening: Mermas weakening: Mermas
total: Total total: Total
wasteDetailLink: 'Para ver el desglose de mermas haz clic en el siguiente enlace:'