Merge pull request '3752-claim_action' (#939) from 3752-claim_action into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #939
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2022-05-10 12:05:03 +00:00
commit d7b76835f5
11 changed files with 428 additions and 91 deletions

View File

@ -731,7 +731,7 @@ export default {
claimAction: {
importClaimButton: 'vn-claim-action vn-button[label="Import claim"]',
anyLine: 'vn-claim-action vn-tbody > vn-tr',
firstDeleteLine: 'vn-claim-action vn-tr:nth-child(1) vn-icon-button[icon="delete"]',
firstDeleteLine: 'vn-claim-action tr:nth-child(1) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
},
ordersIndex: {

View File

@ -0,0 +1,49 @@
module.exports = Self => {
Self.remoteMethodCtx('deleteClamedSales', {
description: 'Deletes the claimed sales',
accessType: 'WRITE',
accepts: [{
arg: 'sales',
type: ['object'],
required: true,
description: 'The sales to remove'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteClamedSales`,
verb: 'POST'
}
});
Self.deleteClamedSales = async(ctx, sales, options) => {
const models = Self.app.models;
const myOptions = {};
const tx = await Self.beginTransaction({});
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction)
myOptions.transaction = tx;
try {
const promises = [];
for (let sale of sales) {
const deletedSale = models.ClaimEnd.destroyById(sale.id, myOptions);
promises.push(deletedSale);
}
const deletedSales = await Promise.all(promises);
if (tx) await tx.commit();
return deletedSales;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,69 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('filter', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}, {
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by client id`,
http: {source: 'query'}
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT *
FROM (
SELECT
ce.id,
ce.claimFk,
s.itemFk,
s.ticketFk,
ce.claimDestinationFk,
t.landed,
s.quantity,
s.concept,
s.price,
s.discount,
s.quantity * s.price * ((100 - s.discount) / 100) total
FROM vn.claimEnd ce
LEFT JOIN vn.sale s ON s.id = ce.saleFk
LEFT JOIN vn.ticket t ON t.id = s.ticketFk
) ce`
);
stmt.merge(conn.makeSuffix(filter));
const itemsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return itemsIndex === 0 ? result : result[itemsIndex];
};
};

View File

@ -0,0 +1,55 @@
module.exports = Self => {
Self.remoteMethod('updateClaimDestination', {
description: 'Update a claim with privileges',
accessType: 'WRITE',
accepts: [{
arg: 'rows',
type: ['object'],
required: true,
description: `the sales which will be modified the claimDestinationFk`
}, {
arg: 'claimDestinationFk',
type: 'number',
required: true
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/updateClaimDestination`,
verb: 'post'
}
});
Self.updateClaimDestination = async(rows, claimDestinationFk, options) => {
const tx = await Self.beginTransaction({});
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction)
myOptions.transaction = tx;
try {
const models = Self.app.models;
const promises = [];
for (let row of rows) {
const claimEnd = await models.ClaimEnd.findById(row.id, null, myOptions);
const updatedClaimEnd = claimEnd.updateAttribute('claimDestinationFk', claimDestinationFk, myOptions);
promises.push(updatedClaimEnd);
}
const updatedSales = await Promise.all(promises);
if (tx) await tx.commit();
return updatedSales;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,4 @@
module.exports = Self => {
require('../methods/claim-end/filter')(Self);
require('../methods/claim-end/deleteClamedSales')(Self);
};

View File

@ -7,5 +7,6 @@ module.exports = Self => {
require('../methods/claim/uploadFile')(Self);
require('../methods/claim/updateClaimAction')(Self);
require('../methods/claim/isEditable')(Self);
require('../methods/claim/updateClaimDestination')(Self);
require('../methods/claim/downloadFile')(Self);
};

View File

@ -1,10 +1,8 @@
<vn-crud-model vn-id="model"
url="ClaimEnds"
filter="$ctrl.filter"
url="ClaimEnds/filter"
link="{claimFk: $ctrl.$params.id}"
data="$ctrl.salesClaimed"
auto-load="true"
auto-save="true"
on-save="$ctrl.onSave()">
</vn-crud-model>
<vn-crud-model
@ -19,101 +17,124 @@
</vn-label-value>
</vn-card>
<vn-card class="vn-pa-lg vn-w-lg">
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button
label="Import claim"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
vn-http-click="$ctrl.importToNewRefundTicket()"
translate-attr="{title: 'Imports claim details'}">
</vn-button>
<vn-range
label="Responsability"
min-label="Company"
max-label="Sales/Client"
ng-model="$ctrl.claim.responsibility"
max="$ctrl.maxResponsibility"
min="1"
step="1"
on-change="$ctrl.save({responsibility: value})">
</vn-range>
</vn-tool-bar>
<vn-check vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"
on-change="$ctrl.save({isChargedToMana: value})">
</vn-check>
</section>
<vn-data-viewer model="model">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th number>Id</vn-th>
<vn-th number>Ticket</vn-th>
<vn-th>Destination</vn-th>
<vn-th expand>Landed</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th>Description</vn-th>
<vn-th number>Price</vn-th>
<vn-th number>Disc.</vn-th>
<vn-th number>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<section class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button
label="Import claim"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
vn-http-click="$ctrl.importToNewRefundTicket()"
translate-attr="{title: 'Imports claim details'}">
</vn-button>
<vn-button
label="Change destination"
disabled="$ctrl.checked.length == 0"
ng-click="changeDestination.show()">
</vn-button>
<vn-range
label="Responsability"
min-label="Company"
max-label="Sales/Client"
ng-model="$ctrl.claim.responsibility"
max="$ctrl.maxResponsibility"
min="1"
step="1"
on-change="$ctrl.save({responsibility: value})">
</vn-range>
</vn-tool-bar>
<vn-check class="right"
vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"
on-change="$ctrl.save({isChargedToMana: value})">
</vn-check>
</section>
</slot-actions>
<slot-table>
<table model="model">
<thead>
<tr>
<th shrink>
<vn-multi-check
model="model"
check-field="$checked">
</vn-multi-check>
</th>
<th number field="itemFk">Id</th>
<th number field="ticketFk">Ticket</th>
<th field="claimDestinationFk">Destination</th>
<th expand field="landed">Landed</th>
<th number field="quantity">Quantity</th>
<th field="concept">Description</th>
<th number field="price">Price</th>
<th number field="discount">Disc.</th>
<th number field="total">Total</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="saleClaimed in $ctrl.salesClaimed"
vn-repeat-last on-last="$ctrl.focusLastInput()">
<vn-td number>
<span
ng-click="itemDescriptor.show($event, saleClaimed.sale.itemFk)"
<td>
<vn-check
ng-model="saleClaimed.$checked"
vn-click-stop>
</vn-check>
</td>
<td number>
<vn-span
ng-click="itemDescriptor.show($event, saleClaimed.itemFk)"
class="link">
{{::saleClaimed.sale.itemFk | zeroFill:6}}
</span>
</vn-td>
<vn-td number>
<span
{{::saleClaimed.itemFk | zeroFill:6}}
</vn-span>
</td>
<td number>
<vn-span
class="link"
ng-click="ticketDescriptor.show($event, saleClaimed.sale.ticketFk)">
{{::saleClaimed.sale.ticketFk}}
</span>
</vn-td>
<vn-td expand>
ng-click="ticketDescriptor.show($event, saleClaimed.ticketFk)">
{{::saleClaimed.ticketFk}}
</vn-span>
</td>
<td expand>
<vn-autocomplete vn-one id="claimDestinationFk"
ng-model="saleClaimed.claimDestinationFk"
data="claimDestinations"
on-change="$ctrl.updateDestination(saleClaimed, value)"
fields="['id','description']"
value-field="id"
show-field="description">
</vn-autocomplete>
</vn-td>
<vn-td expand>{{::saleClaimed.sale.ticket.landed | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td number>{{::saleClaimed.sale.quantity}}</vn-td>
<vn-td expand>{{::saleClaimed.sale.concept}}</vn-td>
<vn-td number>{{::saleClaimed.sale.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{::saleClaimed.sale.discount}} %</vn-td>
<vn-td number>
{{saleClaimed.sale.quantity * saleClaimed.sale.price *
((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}}
</vn-td>
<vn-td shrink>
</td>
<td expand>{{::saleClaimed.landed | date: 'dd/MM/yyyy'}}</td>
<td number>{{::saleClaimed.quantity}}</td>
<td expand>{{::saleClaimed.concept}}</td>
<td number>{{::saleClaimed.price | currency: 'EUR':2}}</td>
<td number>{{::saleClaimed.discount}} %</td>
<td number>{{saleClaimed.total | currency: 'EUR':2}}</td>
<td shrink>
<vn-icon-button
vn-tooltip="Remove line"
icon="delete"
ng-click="model.remove($index)"
ng-click="$ctrl.removeSales(saleClaimed)"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-button-bar>
<vn-button
label="Regularize"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
vn-http-click="$ctrl.regularize()">
</vn-button>
</vn-button-bar>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
<button-bar class="vn-pa-md">
<vn-button
label="Regularize"
disabled="$ctrl.claim.claimStateFk == $ctrl.resolvedStateId"
vn-http-click="$ctrl.regularize()">
</vn-button>
</button-bar>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
@ -127,4 +148,29 @@
question="Insert greuges on client card"
message="Do you want to insert greuges?"
on-accept="$ctrl.onUpdateGreugeAccept()">
</vn-confirm>
</vn-confirm>
<!-- Dialog of change destionation -->
<vn-dialog
vn-id="changeDestination"
on-accept="$ctrl.onResponse()">
<tpl-body>
<section class="SMSDialog">
<h5 class="vn-py-sm">{{$ctrl.$t('Change destination to all selected rows', {total: $ctrl.checked.length})}}</h5>
<vn-horizontal>
<vn-autocomplete vn-one id="claimDestinationFk"
ng-model="$ctrl.newDestination"
data="claimDestinations"
fields="['id','description']"
value-field="id"
show-field="description"
vn-focus>
</vn-autocomplete>
</vn-horizontal>
</section>
</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

@ -5,6 +5,7 @@ import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.newDestination;
this.filter = {
include: [
{relation: 'sale',
@ -21,6 +22,81 @@ export default class Controller extends Section {
};
this.getResolvedState();
this.maxResponsibility = 5;
this.smartTableOptions = {
activeButtons: {
search: true
},
columns: [
{
field: 'claimDestinationFk',
autocomplete: {
url: 'ClaimDestinations',
showField: 'description',
valueField: 'id'
}
},
{
field: 'landed',
searchable: false
}
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'itemFk':
case 'ticketFk':
case 'claimDestinationFk':
case 'quantity':
case 'price':
case 'discount':
case 'total':
return {[param]: value};
case 'concept':
return {[param]: {like: `%${value}%`}};
case 'landed':
return {[param]: {between: this.dateRange(value)}};
}
}
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];
}
get checked() {
const salesClaimed = this.$.model.data || [];
const checkedSalesClaimed = [];
for (let saleClaimed of salesClaimed) {
if (saleClaimed.$checked)
checkedSalesClaimed.push(saleClaimed);
}
return checkedSalesClaimed;
}
updateDestination(saleClaimed, claimDestinationFk) {
const data = {rows: [saleClaimed], claimDestinationFk: claimDestinationFk};
this.$http.post(`Claims/updateClaimDestination`, data).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
}).catch(e => {
this.$.model.refresh();
throw e;
});
}
removeSales(saleClaimed) {
const params = {sales: [saleClaimed]};
this.$http.post(`ClaimEnds/deleteClamedSales`, params).then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
getResolvedState() {
@ -54,8 +130,8 @@ export default class Controller extends Section {
calculateTotals() {
this.claimedTotal = 0;
this.salesClaimed.forEach(sale => {
const price = sale.sale.quantity * sale.sale.price;
const discount = (sale.sale.discount * (sale.sale.quantity * sale.sale.price)) / 100;
const price = sale.quantity * sale.price;
const discount = (sale.discount * (sale.quantity * sale.price)) / 100;
this.claimedTotal += price - discount;
});
}
@ -125,6 +201,24 @@ export default class Controller extends Section {
onSave() {
this.vnApp.showSuccess(this.$t('Data saved!'));
}
onResponse() {
const rowsToEdit = [];
for (let row of this.checked)
rowsToEdit.push({id: row.id});
const data = {
rows: rowsToEdit,
claimDestinationFk: this.newDestination
};
const query = `Claims/updateClaimDestination`;
this.$http.post(query, data)
.then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Data saved!'));
});
}
}
ngModule.vnComponent('vnClaimAction', {

View File

@ -43,9 +43,9 @@ describe('claim', () => {
describe('calculateTotals()', () => {
it('should calculate the total price of the items claimed', () => {
controller.salesClaimed = [
{sale: {quantity: 5, price: 2, discount: 0}},
{sale: {quantity: 10, price: 2, discount: 0}},
{sale: {quantity: 10, price: 2, discount: 0}}
{quantity: 5, price: 2, discount: 0},
{quantity: 10, price: 2, discount: 0},
{quantity: 10, price: 2, discount: 0}
];
controller.calculateTotals();
@ -151,5 +151,17 @@ describe('claim', () => {
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Greuge added');
});
});
describe('onResponse()', () => {
it('should perform a post query and show a success message', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expect('POST', `Claims/updateClaimDestination`).respond({});
controller.onResponse();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
});
});

View File

@ -7,4 +7,7 @@ Regularize: Regularizar
Do you want to insert greuges?: Desea insertar greuges?
Insert greuges on client card: Insertar greuges en la ficha del cliente
Greuge added: Greuge añadido
ClaimGreugeDescription: Reclamación id {{claimId}}
ClaimGreugeDescription: Reclamación id {{claimId}}
Change destination: Cambiar destino
Change destination to all selected rows: Cambiar destino a {{total}} fila(s) seleccionada(s)
Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)

View File

@ -6,7 +6,7 @@ vn-claim-action {
align-content: center;
vn-tool-bar {
flex: 1
flex: none
}
.vn-check {
@ -39,4 +39,8 @@ vn-claim-action {
max-height: 350px;
}
}
.right {
margin-left: 370px;
}
}