Merge pull request '3888-ticket.expedition_moveExpedition' (!1097) from 3888-ticket.expedition_moveExpedition into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #1097
Reviewed-by: Joan Sanchez <joan@verdnatura.es>
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
This commit is contained in:
Vicent Llopis 2022-11-02 13:54:08 +00:00
commit b2cfcab569
13 changed files with 489 additions and 59 deletions

View File

@ -596,7 +596,14 @@ export default {
submitNotesButton: 'button[type=submit]'
},
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'
},
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() => {
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.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 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",
"This route does not exists": "Esta ruta no existe",
"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 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 = [];
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);
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) {
require('../methods/expedition/filter')(Self);
require('../methods/expedition/deleteExpeditions')(Self);
require('../methods/expedition/moveExpeditions')(Self);
};

View File

@ -8,54 +8,77 @@
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-w-xl">
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th></vn-th>
<vn-th field="itemFk" number>Expedition</vn-th>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="packageItemName">Name</vn-th>
<vn-th field="freightItemName">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="created" expand>Created</vn-th>
<vn-th field="state" expand>State</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="expedition in expeditions">
<vn-td class="vn-px-md" style="width:30px; color:#FFA410;">
<vn-icon-button icon="delete"
ng-click="deleteExpedition.show(expedition.id)"
vn-tooltip="Delete expedition">
</vn-icon-button>
</vn-td>
<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 class="vn-pa-lg">
<vn-horizontal class="header">
<vn-tool-bar class="vn-mb-md">
<vn-button icon="keyboard_arrow_down"
label="Move"
ng-click="moreOptions.show($event)"
disabled="!$ctrl.totalChecked">
</vn-button>
<vn-button
disabled="!$ctrl.checked.length"
ng-click="removeConfirm.show()"
icon="delete"
vn-tooltip="Delete expedition">
</vn-button>
</vn-tool-bar>
<vn-one class="taxes" ng-if="$ctrl.sales.length > 0">
<p><vn-label translate>Subtotal</vn-label> {{$ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label translate>VAT</vn-label> {{$ctrl.ticket.totalWithVat - $ctrl.ticket.totalWithoutVat | currency: 'EUR':2}}</p>
<p><vn-label><strong>Total</strong></vn-label> <strong>{{$ctrl.ticket.totalWithVat | currency: 'EUR':2}}</strong></p>
</vn-one>
</vn-horizontal>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="itemFk" number>Expedition</vn-th>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="packageItemName">Name</vn-th>
<vn-th field="freightItemName">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th>
<vn-th field="created" expand>Created</vn-th>
<vn-th field="state" expand>State</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="expedition in expeditions">
<vn-td shrink>
<vn-check tabindex="-1"
ng-model="expedition.checked">
</vn-check>
</vn-td>
<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-data-viewer>
<vn-item-descriptor-popover
@ -66,25 +89,25 @@
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>
<vn-confirm
vn-id="delete-expedition"
on-accept="$ctrl.onDialogAccept($data)"
<vn-confirm
vn-id="removeConfirm"
message="Are you sure you want to delete this expedition?"
question="Delete expedition"
message="Are you sure you want to delete this expedition?">
on-accept="$ctrl.onRemove()">
</vn-confirm>
<vn-popup vn-id="statusLog">
<vn-crud-model
vn-id="model"
vn-id="modelExpeditionStates"
url="ExpeditionStates/filter"
link="{expeditionFk: $ctrl.expedition.id}"
data="expeditionStates"
order="created DESC"
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-data-viewer model="modelExpeditionStates">
<vn-card class="vn-w-md">
<vn-table model="model">
<vn-table model="modelExpeditionStates">
<vn-thead>
<vn-tr>
<vn-th field="state">State</vn-th>
@ -111,4 +134,37 @@
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</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';
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) {
return this.$http.delete(`Expeditions/${id}`)
.then(() => this.$.model.refresh());
@ -11,6 +32,33 @@ class Controller extends Section {
this.expedition = expedition;
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', {

View File

@ -17,6 +17,14 @@ describe('Ticket', () => {
refresh: () => {}
};
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()', () => {
@ -50,5 +58,51 @@ describe('Ticket', () => {
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