changes to percentage filter
gitea/salix/2008-item_buyer_waste_detail This commit looks good Details

This commit is contained in:
Joan Sanchez 2020-01-16 13:39:32 +01:00
parent 1307f6c5fb
commit 2b2e6532f6
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

@ -1135,6 +1135,19 @@ INSERT INTO `bi`.`claims_ratio`(`id_Cliente`, `Consumo`, `Reclamaciones`, `Ratio
(103, 2000, 0.00, 0.00, 0.02, 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`)
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)),

View File

@ -29,7 +29,7 @@ describe('Ticket List sale path', () => {
const value = await nightmare
.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() => {

View File

@ -173,10 +173,10 @@ xdescribe('Ticket Edit sale path', () => {
it('should confirm the discount have been updated', async() => {
const result = await nightmare
.waitForTextInElement(`${selectors.ticketSales.firstSaleDiscount} > span`, '50 %')
.waitForTextInElement(`${selectors.ticketSales.firstSaleDiscount} > span`, '50.00%')
.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() => {

View File

@ -1,15 +1,22 @@
import ngModule from '../module';
/**
* Formats a number multiplying by 100 and adding character %.
*
* @return {String} The formated number
*/
export default function percentage() {
return function(input) {
export default function percentage($translate) {
function percentage(input, fractionSize = 2) {
if (input == null || input === '')
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);

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/getVisibleAvailable')(Self);
require('../methods/item/new')(Self);
require('../methods/item/getWasteDetail')(Self);
Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'});

View File

@ -61,3 +61,4 @@ Diary: Histórico
Item diary: Registro de compra-venta
Last entries: Últimas entradas
Tags: Etiquetas
Waste breakdown: Desglose de mermas

View File

@ -7,7 +7,8 @@
"menus": {
"main": [
{"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": [
{"state": "item.card.basicData", "icon": "settings"},
@ -141,11 +142,8 @@
"url" : "/waste",
"state": "item.waste",
"component": "vn-item-waste",
"description": "Waste",
"params": {
"item": "$ctrl.item"
},
"acl": ["employee"]
"description": "Waste breakdown",
"acl": ["buyer"]
}
]
}

View File

@ -1,120 +1,32 @@
<vn-crud-model auto-load="true"
vn-id="model"
url="Items/filter"
limit="20"
data="requests"
order="shipped DESC, isOk ASC">
url="Items/getWasteDetail"
data="details">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="buer" number>Buyer</vn-th>
<vn-th field="family">Family</vn-th>
<vn-th field="percentage">Percentage</vn-th>
<vn-th field="salesPersonNickname">Mermas</vn-th>
<vn-th field="total">Total</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>
<span title="{{::request.shipped | date: 'dd/MM/yyyy'}}"
class="chip {{$ctrl.compareDate(request.shipped)}}">
{{::request.shipped | date: 'dd/MM/yyyy'}}
</span>
</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>
<section ng-repeat="detail in details" class="vn-pa-md">
<vn-horizontal class="header">
<h5><span translate>{{detail.buyer}}</span></h5>
</vn-horizontal>
<vn-table>
<vn-thead>
<vn-tr>
<vn-th>Family</vn-th>
<vn-th shrink>Percentage</vn-th>
<vn-th number>Dwindle</vn-th>
<vn-th number>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="waste in detail.lines">
<vn-td>{{::waste.family}}</vn-td>
<vn-td shrink>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
<vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>
<vn-td number>{{::waste.total | currency: 'EUR'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</section>
</vn-card>
</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 './style.scss';
export default class Controller extends Component {
}
ngModule.component('vnItemWaste', {
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
Specify the reasons to deny this request: Especifica las razones para descartar la petición
Buy requests: Peticiones de compra
Search request by id or alias: Buscar peticiones por identificador o alias
Requested: Solicitado
Achieved: Conseguido
Family: Familia
Percentage: Porcentaje
Dwindle: Mermas

View File

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

View File

@ -4,5 +4,6 @@ module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`])
`${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.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>
</tbody>
</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>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">

View File

@ -6,3 +6,4 @@ buyer: Comprador
percentage: Porcentaje
weakening: Mermas
total: Total
wasteDetailLink: 'Para ver el desglose de mermas haz clic en el siguiente enlace:'