3888-ticket.expedition_moveExpedition #1097

Merged
vicent merged 26 commits from 3888-ticket.expedition_moveExpedition into dev 2022-11-02 13:54:11 +00:00
13 changed files with 489 additions and 59 deletions

View File

@ -596,7 +596,14 @@ export default {
submitNotesButton: 'button[type=submit]' submitNotesButton: 'button[type=submit]'
}, },
ticketExpedition: { ticketExpedition: {
thirdExpeditionRemoveButton: 'vn-ticket-expedition vn-table div > vn-tbody > vn-tr:nth-child(3) > vn-td:nth-child(1) > vn-icon-button[icon="delete"]', firstSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(1) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(3) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="keyboard_arrow_down"]',
moreMenuWithoutRoute: 'vn-item[name="withoutRoute"]',
moreMenuWithRoute: 'vn-item[name="withRoute"]',
newRouteId: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newRoute"]',
saveButton: '.vn-dialog.shown [response="accept"]',
expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr' expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr'
}, },
ticketPackages: { ticketPackages: {

View File

@ -18,7 +18,8 @@ describe('Ticket expeditions and log path', () => {
}); });
it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => { it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdExpeditionRemoveButton); await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.deleteExpeditionButton);
await page.waitToClick(selectors.globalItems.acceptButton); await page.waitToClick(selectors.globalItems.acceptButton);
await page.reloadSection('ticket.card.expedition'); await page.reloadSection('ticket.card.expedition');

View File

@ -0,0 +1,50 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Ticket expeditions', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('production', 'ticket');
await page.accessToSearchResult('1');
await page.accessToSection('ticket.card.expedition');
});
afterAll(async() => {
await browser.close();
});
it(`should move one expedition to new ticket withoute route`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithoutRoute);
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
it(`should move one expedition to new ticket with route`, async() => {
await page.waitToClick(selectors.ticketExpedition.firstSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithRoute);
await page.write(selectors.ticketExpedition.newRouteId, '1');
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
});

View File

@ -236,6 +236,7 @@
"Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador", "Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador",
"Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador", "Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador",
"Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente", "Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente",
"This route does not exists": "Esta ruta no existe",
vicent marked this conversation as resolved Outdated
Outdated
Review

This route does not exists

This route does not exists
"Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*", "Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*",
"You don't have grant privilege": "No tienes privilegios para dar privilegios", "You don't have grant privilege": "No tienes privilegios para dar privilegios",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario" "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario"

View File

@ -0,0 +1,52 @@
module.exports = Self => {
Self.remoteMethod('deleteExpeditions', {
description: 'Delete the selected expeditions',
accessType: 'WRITE',
accepts: [{
arg: 'expeditionIds',
type: ['number'],
required: true,
description: 'The expeditions ids to delete'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteExpeditions`,
verb: 'POST'
}
});
Self.deleteExpeditions = async(expeditionIds, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const promises = [];
vicent marked this conversation as resolved Outdated
Outdated
Review

Utilizar destroyById() en un bucle. La función destroyAll() no es segura

Utilizar destroyById() en un bucle. La función destroyAll() no es segura
for (let expeditionId of expeditionIds) {
const deletedExpedition = models.Expedition.destroyById(expeditionId, myOptions);
promises.push(deletedExpedition);
}
const deletedExpeditions = await Promise.all(promises);
if (tx) await tx.commit();
return deletedExpeditions;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,93 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('moveExpeditions', {
description: 'Move the selected expeditions to another ticket',
accessType: 'WRITE',
accepts: [{
arg: 'clientId',
type: 'number',
description: `The client id`,
required: true
},
{
arg: 'landed',
type: 'date',
description: `The landing date`
},
{
arg: 'warehouseId',
type: 'number',
description: `The warehouse id`,
required: true
},
{
arg: 'addressId',
type: 'number',
description: `The address id`,
required: true
},
{
arg: 'agencyModeId',
type: 'any',
description: `The agencyMode id`
},
{
arg: 'routeId',
type: 'any',
description: `The route id`
},
{
arg: 'expeditionIds',
type: ['number'],
required: true,
description: 'The expeditions ids to move'
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/moveExpeditions`,
verb: 'POST'
}
});
Self.moveExpeditions = async(ctx, options) => {
const models = Self.app.models;
const args = ctx.args;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
if (args.routeId) {
const route = await models.Route.findById(args.routeId, null, myOptions);
if (!route) throw new UserError('This route does not exists');
}
const ticket = await models.Ticket.new(ctx, myOptions);
const promises = [];
for (let expeditionsId of args.expeditionIds) {
const expeditionToUpdate = await models.Expedition.findById(expeditionsId, null, myOptions);
vicent marked this conversation as resolved Outdated
Outdated
Review

pasar transacción null, myOptions

pasar transacción null, myOptions
const expeditionUpdated = expeditionToUpdate.updateAttribute('ticketFk', ticket.id, myOptions);
promises.push(expeditionUpdated);
}
await Promise.all(promises);
if (tx) await tx.commit();
return ticket;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,22 @@
const models = require('vn-loopback/server/server').models;
describe('ticket deleteExpeditions()', () => {
it('should delete the selected expeditions', async() => {
const tx = await models.Expedition.beginTransaction({});
try {
const options = {transaction: tx};
const expeditionIds = [12, 13];
const result = await models.Expedition.deleteExpeditions(expeditionIds, options);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,39 @@
const models = require('vn-loopback/server/server').models;
describe('ticket moveExpeditions()', () => {
it('should move the selected expeditions to new ticket', async() => {
const tx = await models.Expedition.beginTransaction({});
const ctx = {
req: {accessToken: {userId: 9}},
args: {},
params: {}
};
const myCtx = Object.assign({}, ctx);
try {
const options = {transaction: tx};
myCtx.args = {
clientId: 1101,
landed: new Date(),
warehouseId: 1,
addressId: 121,
agencyModeId: 1,
routeId: null,
expeditionIds: [1, 2]
};
const ticket = await models.Expedition.moveExpeditions(myCtx, options);
const newestTicketIdInFixtures = 27;
expect(ticket.id).toBeGreaterThan(newestTicketIdInFixtures);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -1,3 +1,5 @@
module.exports = function(Self) { module.exports = function(Self) {
require('../methods/expedition/filter')(Self); require('../methods/expedition/filter')(Self);
require('../methods/expedition/deleteExpeditions')(Self);
require('../methods/expedition/moveExpeditions')(Self);
}; };

View File

@ -8,54 +8,77 @@
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model"> <vn-data-viewer model="model">
<vn-card class="vn-w-xl"> <vn-card class="vn-pa-lg">
<vn-table model="model"> <vn-horizontal class="header">
<vn-thead> <vn-tool-bar class="vn-mb-md">
<vn-tr> <vn-button icon="keyboard_arrow_down"
<vn-th></vn-th> label="Move"
<vn-th field="itemFk" number>Expedition</vn-th> ng-click="moreOptions.show($event)"
<vn-th field="itemFk" number>Item</vn-th> disabled="!$ctrl.totalChecked">
<vn-th field="packageItemName">Name</vn-th> </vn-button>
<vn-th field="freightItemName">Package type</vn-th> <vn-button
<vn-th field="counter" number>Counter</vn-th> disabled="!$ctrl.checked.length"
<vn-th field="externalId" number>externalId</vn-th> ng-click="removeConfirm.show()"
<vn-th field="created" expand>Created</vn-th> icon="delete"
<vn-th field="state" expand>State</vn-th> vn-tooltip="Delete expedition">
<vn-th></vn-th> </vn-button>
</vn-tr> </vn-tool-bar>
</vn-thead> <vn-one class="taxes" ng-if="$ctrl.sales.length > 0">
<vn-tbody> <p><vn-label translate>Subtotal</vn-label> {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<vn-tr ng-repeat="expedition in expeditions"> <p><vn-label translate>VAT</vn-label> {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<vn-td class="vn-px-md" style="width:30px; color:#FFA410;"> <p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}</strong></p>
<vn-icon-button icon="delete" </vn-one>
ng-click="deleteExpedition.show(expedition.id)" </vn-horizontal>
vn-tooltip="Delete expedition"> <vn-table model="model">
</vn-icon-button> <vn-thead>
</vn-td> <vn-tr>
<vn-td number expand>{{expedition.id | zeroFill:6}}</vn-td> <vn-th shrink>
<vn-td number> <vn-multi-check
<span model="model">
ng-class="{link: expedition.packagingItemFk}" </vn-multi-check>
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)"> </vn-th>
{{expedition.packagingFk}} <vn-th field="itemFk" number>Expedition</vn-th>
</span> <vn-th field="itemFk" number>Item</vn-th>
</vn-td> <vn-th field="packageItemName">Name</vn-th>
<vn-td>{{::expedition.packageItemName}}</vn-td> <vn-th field="freightItemName">Package type</vn-th>
<vn-td>{{::expedition.freightItemName}}</vn-td> <vn-th field="counter" number>Counter</vn-th>
<vn-td number>{{::expedition.counter}}</vn-td> <vn-th field="externalId" number>externalId</vn-th>
<vn-td expand>{{::expedition.externalId}}</vn-td> <vn-th field="created" expand>Created</vn-th>
<vn-td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td> <vn-th field="state" expand>State</vn-th>
<vn-td>{{::expedition.state}}</vn-td> <vn-th></vn-th>
<vn-td> </vn-tr>
<vn-icon-button </vn-thead>
vn-click-stop="$ctrl.showLog(expedition)" <vn-tbody>
vn-tooltip="Status log" <vn-tr ng-repeat="expedition in expeditions">
icon="history"> <vn-td shrink>
</vn-icon-button> <vn-check tabindex="-1"
</vn-td> ng-model="expedition.checked">
</vn-tr> </vn-check>
</vn-tbody> </vn-td>
</vn-table> <vn-td number expand>{{expedition.id | zeroFill:6}}</vn-td>
<vn-td number>
<span
ng-class="{link: expedition.packagingItemFk}"
ng-click="itemDescriptor.show($event, expedition.packagingItemFk)">
{{expedition.packagingFk}}
</span>
</vn-td>
<vn-td>{{::expedition.packageItemName}}</vn-td>
<vn-td>{{::expedition.freightItemName}}</vn-td>
<vn-td number>{{::expedition.counter}}</vn-td>
<vn-td expand>{{::expedition.externalId}}</vn-td>
<vn-td shrink-datetime>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td>{{::expedition.state}}</vn-td>
<vn-td>
<vn-icon-button
vn-click-stop="$ctrl.showLog(expedition)"
vn-tooltip="Status log"
icon="history">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>
<vn-item-descriptor-popover <vn-item-descriptor-popover
@ -66,25 +89,25 @@
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="worker-descriptor"> vn-id="worker-descriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
<vn-confirm <vn-confirm
vn-id="delete-expedition" vn-id="removeConfirm"
on-accept="$ctrl.onDialogAccept($data)" message="Are you sure you want to delete this expedition?"
question="Delete expedition" question="Delete expedition"
message="Are you sure you want to delete this expedition?"> on-accept="$ctrl.onRemove()">
</vn-confirm> </vn-confirm>
<vn-popup vn-id="statusLog"> <vn-popup vn-id="statusLog">
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="modelExpeditionStates"
url="ExpeditionStates/filter" url="ExpeditionStates/filter"
link="{expeditionFk: $ctrl.expedition.id}" link="{expeditionFk: $ctrl.expedition.id}"
data="expeditionStates" data="expeditionStates"
order="created DESC" order="created DESC"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model"> <vn-data-viewer model="modelExpeditionStates">
<vn-card class="vn-w-md"> <vn-card class="vn-w-md">
<vn-table model="model"> <vn-table model="modelExpeditionStates">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
<vn-th field="state">State</vn-th> <vn-th field="state">State</vn-th>
@ -111,4 +134,37 @@
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="workerDescriptor"> vn-id="workerDescriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
</vn-popup> </vn-popup>
<vn-menu vn-id="moreOptions">
<vn-item translate
name="withoutRoute"
ng-click="selectLanded.show('withoutRoute')">
New ticket without route
</vn-item>
<vn-item translate
name="withRoute"
ng-click="selectLanded.show('withRoute')">
New ticket with route
</vn-item>
</vn-menu>
<vn-dialog
vn-id="selectLanded"
on-accept="$ctrl.createTicket($ctrl.landed, $ctrl.newRoute)">
<tpl-body>
<vn-date-picker
label="Landed"
ng-model="$ctrl.landed">
</vn-date-picker>
<vn-textfield
ng-show="selectLanded.data == 'withRoute'"
label="Route id"
ng-model="$ctrl.newRoute">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Accept</button>
</tpl-buttons>
</vn-dialog>

View File

@ -2,6 +2,27 @@ import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
class Controller extends Section { class Controller extends Section {
constructor($element, $scope) {
super($element, $scope);
this.landed = new Date();
this.newRoute = null;
}
get checked() {
const rows = this.$.model.data || [];
const checkedRows = [];
for (let row of rows) {
if (row.checked)
checkedRows.push(row.id);
}
return checkedRows;
}
get totalChecked() {
return this.checked.length;
}
onDialogAccept(id) { onDialogAccept(id) {
return this.$http.delete(`Expeditions/${id}`) return this.$http.delete(`Expeditions/${id}`)
.then(() => this.$.model.refresh()); .then(() => this.$.model.refresh());
@ -11,6 +32,33 @@ class Controller extends Section {
this.expedition = expedition; this.expedition = expedition;
this.$.statusLog.show(); this.$.statusLog.show();
} }
onRemove() {
const params = {expeditionIds: this.checked};
const query = `Expeditions/deleteExpeditions`;
this.$http.post(query, params)
.then(() => {
this.vnApp.showSuccess(this.$t('Expedition removed'));
this.$.model.refresh();
});
}
createTicket(landed, routeFk) {
const params = {
clientId: this.ticket.clientFk,
landed: landed,
warehouseId: this.ticket.warehouseFk,
addressId: this.ticket.addressFk,
agencyModeId: this.ticket.agencyModeFk,
routeId: routeFk,
expeditionIds: this.checked
};
const query = `Expeditions/moveExpeditions`;
this.$http.post(query, params).then(res => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$state.go('ticket.card.summary', {id: res.data.id});
});
}
} }
ngModule.vnComponent('vnTicketExpedition', { ngModule.vnComponent('vnTicketExpedition', {

View File

@ -17,6 +17,14 @@ describe('Ticket', () => {
refresh: () => {} refresh: () => {}
}; };
controller = $componentController('vnTicketExpedition', {$element: null, $scope}); controller = $componentController('vnTicketExpedition', {$element: null, $scope});
controller.$.model.data = [
{id: 1},
{id: 2},
{id: 3}
];
const modelData = controller.$.model.data;
modelData[0].checked = true;
modelData[1].checked = true;
})); }));
describe('onDialogAccept()', () => { describe('onDialogAccept()', () => {
@ -50,5 +58,51 @@ describe('Ticket', () => {
expect(controller.$.statusLog.show).toHaveBeenCalledWith(); expect(controller.$.statusLog.show).toHaveBeenCalledWith();
}); });
}); });
describe('onRemove()', () => {
it('should make a query and then call to the model refresh() method', () => {
jest.spyOn($scope.model, 'refresh');
const expectedParams = {expeditionIds: [1, 2]};
$httpBackend.expect('POST', 'Expeditions/deleteExpeditions', expectedParams).respond(200);
controller.onRemove();
$httpBackend.flush();
expect($scope.model.refresh).toHaveBeenCalledWith();
});
});
describe('createTicket()', () => {
it('should make a query and then call to the $state go() method', () => {
jest.spyOn(controller.$state, 'go').mockReturnThis();
const ticket = {
clientFk: 1101,
landed: new Date(),
addressFk: 121,
agencyModeFk: 1,
warehouseFk: 1
};
const routeId = null;
controller.ticket = ticket;
const ticketToTransfer = {id: 28};
const expectedParams = {
clientId: 1101,
landed: new Date(),
warehouseId: 1,
addressId: 121,
agencyModeId: 1,
routeId: null,
expeditionIds: [1, 2]
};
$httpBackend.expect('POST', 'Expeditions/moveExpeditions', expectedParams).respond(ticketToTransfer);
controller.createTicket(ticket.landed, routeId);
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.summary', {id: ticketToTransfer.id});
});
});
}); });
}); });

View File

@ -1 +1,6 @@
Status log: Hitorial de estados Status log: Hitorial de estados
Expedition removed: Expedición eliminada
Move: Mover
New ticket without route: Nuevo ticket sin ruta
New ticket with route: Nuevo ticket con ruta
Route id: Id ruta
vicent marked this conversation as resolved Outdated
Outdated
Review

Route id

Route id