transfer partially sale #1251
gitea/salix/dev This commit looks good Details

This commit is contained in:
Joan Sanchez 2019-08-09 08:04:44 +02:00
parent a5486ec694
commit 0918f154e9
21 changed files with 508 additions and 330 deletions

View File

@ -0,0 +1,6 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Dms', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('ClaimDms', 'removeFile', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('ClaimDms', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Claim', 'uploadFile', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -1838,7 +1838,7 @@ INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `c
(17, 'cmr', 'cmr', NULL, NULL, 'cmr'), (17, 'cmr', 'cmr', NULL, NULL, 'cmr'),
(18, 'dua', 'dua', NULL, NULL, 'dua'), (18, 'dua', 'dua', NULL, NULL, 'dua'),
(19, 'inmovilizado', 'inmovilizado', NULL, NULL, 'fixedAssets'), (19, 'inmovilizado', 'inmovilizado', NULL, NULL, 'fixedAssets'),
(20, 'Reclamación', 'reclamacion', NULL, NULL, 'claim'); (20, 'Reclamación', 'reclamacion', 1, 1, 'claim');
INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`) INSERT INTO `vn`.`dms`(`id`, `dmsTypeFk`, `file`, `contentType`, `workerFk`, `warehouseFk`, `companyFk`, `hardCopyNumber`, `hasFile`, `reference`, `description`, `created`)
VALUES VALUES

View File

@ -422,7 +422,7 @@ export default {
thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[field="sale.checked"] md-checkbox', thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[field="sale.checked"] md-checkbox',
deleteSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="delete"]', deleteSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="delete"]',
transferSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="call_split"]', transferSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="call_split"]',
moveToTicketInput: 'vn-ticket-sale vn-popover.transfer vn-textfield[model="$ctrl.receiverTicketId"] input', moveToTicketInput: 'vn-ticket-sale vn-popover.transfer vn-textfield[model="$ctrl.transfer.ticketId"] input',
moveToTicketInputClearButton: 'vn-popover.shown i[title="Clear"]', moveToTicketInputClearButton: 'vn-popover.shown i[title="Clear"]',
moveToTicketButton: 'vn-ticket-sale vn-popover.transfer vn-icon[icon="arrow_forward_ios"]', moveToTicketButton: 'vn-ticket-sale vn-popover.transfer vn-icon[icon="arrow_forward_ios"]',
moveToNewTicketButton: 'vn-ticket-sale vn-popover.transfer vn-button[label="New ticket"]', moveToNewTicketButton: 'vn-ticket-sale vn-popover.transfer vn-button[label="New ticket"]',

View File

@ -1,35 +1,39 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('threeLastActive', { Self.remoteMethod('lastActiveTickets', {
description: 'Returns the last three tickets of a client that have the alertLevel at 0 and the shiped day is gt today', description: 'Returns the last three tickets of a client that have the alertLevel at 0 and the shiped day is gt today',
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'filter', arg: 'id',
type: 'object', type: 'Number',
required: true, required: true,
description: 'client id, ticketFk' description: 'Client id',
http: {source: 'path'}
}, {
arg: 'ticketId',
type: 'Number',
required: true
}], }],
returns: { returns: {
type: [this.modelName], type: ['Object'],
root: true root: true
}, },
http: { http: {
path: `/threeLastActive`, path: `/:id/lastActiveTickets`,
verb: 'GET' verb: 'GET'
} }
}); });
Self.threeLastActive = async params => { Self.lastActiveTickets = async(id, ticketId) => {
let query = ` const query = `
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName
FROM vn.ticket t FROM vn.ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id JOIN vn.warehouse w ON t.warehouseFk = w.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0 AND t.id <> ?
AND ts.alertLevel = 0 AND t.id <> ?
ORDER BY t.shipped ORDER BY t.shipped
LIMIT 5`; LIMIT 3`;
let tickets = await Self.rawSql(query, [params.clientFk, params.ticketFk]);
return tickets; return Self.rawSql(query, [id, ticketId]);
}; };
}; };

View File

@ -0,0 +1,12 @@
const app = require('vn-loopback/server/server');
describe('client lastActiveTickets()', () => {
it('should return the last three active tickets', async() => {
const clientId = 109;
const ticketId = 19;
let result = await app.models.Client.lastActiveTickets(clientId, ticketId);
expect(result.length).toEqual(3);
});
});

View File

@ -21,6 +21,7 @@ module.exports = Self => {
require('../methods/client/confirmTransaction')(Self); require('../methods/client/confirmTransaction')(Self);
require('../methods/client/canBeInvoiced')(Self); require('../methods/client/canBeInvoiced')(Self);
require('../methods/client/uploadFile')(Self); require('../methods/client/uploadFile')(Self);
require('../methods/client/lastActiveTickets')(Self);
// Validations // Validations

View File

@ -1,90 +0,0 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('moveToTicket', {
description: 'Moves lines to a new or a given ticket',
accepts: [{
arg: 'params',
type: 'object',
required: true,
description: 'currentTicket, receiverTicket, [sales IDs], removeEmptyTicket',
http: {source: 'body'}
}],
returns: {
type: 'string',
root: true
},
http: {
path: `/moveToTicket`,
verb: 'post'
}
});
Self.moveToTicket = async(ctx, params) => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let currentTicket = await models.Ticket.findById(params.currentTicket.currentTicketId);
let newTicketData = {};
let receiverTicket = params.receiverTicket;
let isCurrentTicketEditable = await models.Ticket.isEditable(ctx, params.currentTicket.currentTicketId);
if (!isCurrentTicketEditable)
throw new UserError(`The sales of the current ticket can't be modified`);
if (params.receiverTicket.id) {
let isReceiverTicketEditable = await models.Ticket.isEditable(ctx, params.receiverTicket.id);
if (!isReceiverTicketEditable)
throw new UserError(`The sales of the receiver ticket can't be modified`);
}
if (!params.receiverTicket.id) {
let travelDates = await models.Agency.getFirstShipped(params.currentTicket);
if (!travelDates)
throw new UserError(`Invalid parameters to create a new ticket`);
let shipped = new Date(travelDates.shipped);
let landed = new Date(travelDates.landed);
newTicketData = {
clientFk: params.currentTicket.clientFk,
addressFk: params.currentTicket.addressFk,
agencyModeFk: params.currentTicket.agencyModeFk,
warehouseFk: params.currentTicket.warehouseFk,
shipped: shipped,
landed: landed,
userId: userId
};
}
let tx = await Self.beginTransaction({});
try {
let options = {transaction: tx};
if (!params.receiverTicket.id)
receiverTicket = await models.Ticket.new(ctx, newTicketData, options);
let promises = [];
for (let sale of params.sales) {
promises.push(
models.Sale.update(
{id: sale.id},
{ticketFk: receiverTicket.id},
options
)
);
}
if (params.removeEmptyTicket)
promises.push(currentTicket.updateAttributes({isDeleted: true}, options));
await Promise.all(promises);
await tx.commit();
return receiverTicket;
} catch (error) {
await tx.rollback();
throw error;
}
};
};

View File

@ -1,37 +0,0 @@
module.exports = function(Self) {
Self.remoteMethod('checkEmptiness', {
description: 'Checks if the ticket has no packages, componenets and purchase requests',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'Ticket id',
http: {source: 'path'}
}
],
returns: {
arg: 'data',
type: 'boolean',
root: true
},
http: {
path: `/:id/checkEmptiness`,
verb: 'get'
}
});
Self.checkEmptiness = async id => {
const packages = await Self.app.models.TicketPackaging.find({where: {ticketFk: id}});
const services = await Self.app.models.TicketService.find({where: {ticketFk: id}});
const purchaseRequests = await Self.app.models.TicketRequest.find({where: {ticketFk: id}});
emptyTicket = !packages.length && !services.length && !purchaseRequests.length;
if (emptyTicket)
return true;
return false;
};
};

View File

@ -0,0 +1,51 @@
module.exports = function(Self) {
Self.remoteMethod('isEmpty', {
description: 'Checks if the ticket has no packages, componenets and purchase requests',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'Ticket id',
http: {source: 'path'}
}
],
returns: {
type: 'Boolean',
root: true
},
http: {
path: `/:id/isEmpty`,
verb: 'GET'
}
});
Self.isEmpty = async(id, options) => {
const models = Self.app.models;
if ((typeof options) != 'object')
options = {};
const hasSales = await models.Sale.count({
ticketFk: id
}, options);
const hasPackages = await models.TicketPackaging.count({
ticketFk: id
}, options);
const hasServices = await models.TicketService.count({
ticketFk: id
}, options);
const hasPurchaseRequests = await models.TicketRequest.count({
ticketFk: id
}, options);
const isEmpty = !hasSales && !hasPackages &&
!hasServices && !hasPurchaseRequests;
return isEmpty;
};
};

View File

@ -1,27 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket checkEmptiness()', () => {
it('should return false if the ticket contains any packages', async() => {
let result = await app.models.Ticket.checkEmptiness(3);
expect(result).toBeFalsy();
});
it('should return false if the ticket contains any services', async() => {
let result = await app.models.Ticket.checkEmptiness(8);
expect(result).toBeFalsy();
});
it('should return false if the ticket contains any purchase request', async() => {
let result = await app.models.Ticket.checkEmptiness(11);
expect(result).toBeFalsy();
});
it('should return true if the ticket does not contain any packages, services or purchase request', async() => {
let result = await app.models.Ticket.checkEmptiness(4);
expect(result).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
const app = require('vn-loopback/server/server');
describe('ticket isEmpty()', () => {
it('should return false if the ticket contains any packages', async() => {
let result = await app.models.Ticket.isEmpty(3);
expect(result).toBeFalsy();
});
it('should return false if the ticket contains any services', async() => {
let result = await app.models.Ticket.isEmpty(8);
expect(result).toBeFalsy();
});
it('should return false if the ticket contains any purchase request', async() => {
let result = await app.models.Ticket.isEmpty(11);
expect(result).toBeFalsy();
});
it('should return false if the ticket contains any sale', async() => {
let result = await app.models.Ticket.isEmpty(4);
expect(result).toBeFalsy();
});
});

View File

@ -1,10 +0,0 @@
const app = require('vn-loopback/server/server');
describe('ticket threeLastActive()', () => {
it('should return the last three active tickets', async() => {
let params = {clientFk: 109, ticketFk: 19};
let result = await app.models.Ticket.threeLastActive(params);
expect(result.length).toEqual(3);
});
});

View File

@ -1,6 +1,6 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('sale moveToTicket()', () => { describe('sale transferSales()', () => {
let createdTicketId; let createdTicketId;
afterAll(async done => { afterAll(async done => {
@ -12,11 +12,13 @@ describe('sale moveToTicket()', () => {
const ctx = {req: {accessToken: {userId: 101}}}; const ctx = {req: {accessToken: {userId: 101}}};
let error; let error;
const params = {currentTicket: {currentTicketId: 10}}; const currentTicketId = 10;
const receiverTicketId = undefined;
const sales = [];
await app.models.Sale.moveToTicket(ctx, params) await app.models.Ticket.transferSales(ctx, currentTicketId, receiverTicketId, sales)
.catch(response => { .catch(response => {
expect(response.message).toEqual(`The sales of the current ticket can't be modified`); expect(response.message).toEqual(`The sales of this ticket can't be modified`);
error = response; error = response;
}); });
@ -27,9 +29,11 @@ describe('sale moveToTicket()', () => {
const ctx = {req: {accessToken: {userId: 101}}}; const ctx = {req: {accessToken: {userId: 101}}};
let error; let error;
const params = {currentTicket: {currentTicketId: 16}, receiverTicket: {id: 1}}; const currentTicketId = 16;
const receiverTicketId = 1;
const sales = [];
await app.models.Sale.moveToTicket(ctx, params) await app.models.Ticket.transferSales(ctx, currentTicketId, receiverTicketId, sales)
.catch(response => { .catch(response => {
expect(response.message).toEqual(`The sales of the receiver ticket can't be modified`); expect(response.message).toEqual(`The sales of the receiver ticket can't be modified`);
error = response; error = response;
@ -42,9 +46,11 @@ describe('sale moveToTicket()', () => {
const ctx = {req: {accessToken: {userId: 101}}}; const ctx = {req: {accessToken: {userId: 101}}};
let error; let error;
const params = {currentTicket: {currentTicketId: 18}, receiverTicket: {id: undefined}}; const currentTicketId = 18;
const receiverTicketId = undefined;
const sales = [];
await app.models.Sale.moveToTicket(ctx, params) await app.models.Ticket.transferSales(ctx, currentTicketId, receiverTicketId, sales)
.catch(response => { .catch(response => {
expect(response.message).toEqual(`Invalid parameters to create a new ticket`); expect(response.message).toEqual(`Invalid parameters to create a new ticket`);
error = response; error = response;
@ -56,26 +62,19 @@ describe('sale moveToTicket()', () => {
it('should transfer the sales from one ticket to a new one', async() => { it('should transfer the sales from one ticket to a new one', async() => {
const ctx = {req: {accessToken: {userId: 101}}}; const ctx = {req: {accessToken: {userId: 101}}};
let currentTicket = await app.models.Ticket.findById(11); let currentTicket = await app.models.Ticket.findById(11);
currentTicket.currentTicketId = currentTicket.id; let currentTicketSales = await app.models.Ticket.getSales(currentTicket.id);
currentTicket.id = undefined;
let currentTicketSales = await app.models.Ticket.getSales(currentTicket.currentTicketId);
expect(currentTicketSales.length).toEqual(2); expect(currentTicketSales.length).toEqual(2);
let params = { const currentTicketId = currentTicket.id;
currentTicket: currentTicket, const receiverTicketId = undefined;
receiverTicket: {id: undefined}, const sales = currentTicketSales;
sales: [
{id: currentTicketSales[0].id},
{id: currentTicketSales[1].id}
]
};
let createdTicket = await app.models.Sale.moveToTicket(ctx, params); let createdTicket = await app.models.Ticket.transferSales(
ctx, currentTicketId, receiverTicketId, sales);
createdTicketId = createdTicket.id; createdTicketId = createdTicket.id;
currentTicketSales = await app.models.Ticket.getSales(currentTicket.currentTicketId); currentTicketSales = await app.models.Ticket.getSales(currentTicket.id);
receiverTicketSales = await app.models.Ticket.getSales(createdTicket.id); receiverTicketSales = await app.models.Ticket.getSales(createdTicket.id);
expect(currentTicketSales.length).toEqual(0); expect(currentTicketSales.length).toEqual(0);
@ -84,33 +83,24 @@ describe('sale moveToTicket()', () => {
it('should transfer back the sales and set the created ticket as deleted', async() => { it('should transfer back the sales and set the created ticket as deleted', async() => {
const ctx = {req: {accessToken: {userId: 101}}}; const ctx = {req: {accessToken: {userId: 101}}};
let receiverTicketId = 11; const currentTicket = await app.models.Ticket.findById(createdTicketId);
let currentTicket = await app.models.Ticket.findById(createdTicketId); const receiverTicketId = 11;
currentTicket.currentTicketId = createdTicketId;
currentTicket.id = undefined;
let createdTicket = await app.models.Ticket.findById(createdTicketId); let createdTicket = await app.models.Ticket.findById(createdTicketId);
let createdTicketSales = await app.models.Ticket.getSales(createdTicketId); let createdTicketSales = await app.models.Ticket.getSales(createdTicketId);
let receiverTicketSales = await app.models.Ticket.getSales(receiverTicketId); let receiverTicketSales = await app.models.Ticket.getSales(receiverTicketId);
const currentTicketId = currentTicket.id;
const sales = createdTicketSales;
expect(createdTicket.isDeleted).toBeFalsy(); expect(createdTicket.isDeleted).toBeFalsy();
expect(createdTicketSales.length).toEqual(2); expect(createdTicketSales.length).toEqual(2);
expect(receiverTicketSales.length).toEqual(0); expect(receiverTicketSales.length).toEqual(0);
let params = { await app.models.Ticket.transferSales(
removeEmptyTicket: true, ctx, currentTicketId, receiverTicketId, sales);
currentTicket: currentTicket,
receiverTicket: {id: receiverTicketId},
sales: [
{id: createdTicketSales[0].id},
{id: createdTicketSales[1].id}
]
};
await app.models.Sale.moveToTicket(ctx, params);
createdTicket = await app.models.Ticket.findById(createdTicketId); createdTicket = await app.models.Ticket.findById(createdTicketId);
createdTicketSales = await app.models.Ticket.getSales(createdTicketId); createdTicketSales = await app.models.Ticket.getSales(createdTicketId);
receiverTicketSales = await app.models.Ticket.getSales(receiverTicketId); receiverTicketSales = await app.models.Ticket.getSales(receiverTicketId);

View File

@ -0,0 +1,158 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('transferSales', {
description: 'Transfer sales to a new or a given ticket',
accepts: [{
arg: 'id',
type: 'Number',
required: true,
description: 'Origin ticket id',
http: {source: 'path'}
},
{
arg: 'ticketId',
type: 'Number',
description: 'Destination ticket id',
required: false
},
{
arg: 'sales',
type: ['Object'],
description: 'The sales to transfer',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/transferSales`,
verb: 'POST'
}
});
Self.transferSales = async(ctx, id, ticketId, sales) => {
const models = Self.app.models;
const isEditable = await models.Ticket.isEditable(ctx, id);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
if (ticketId) {
const isReceiverEditable = await models.Ticket.isEditable(ctx, ticketId);
if (!isReceiverEditable)
throw new UserError(`The sales of the receiver ticket can't be modified`);
}
let tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const originalTicket = await models.Ticket.findById(id, null, options);
const originalSales = await models.Sale.find({
where: {ticketFk: id}
}, options);
if (!ticketId)
ticketId = await cloneTicket(ctx, originalTicket, options);
const map = new Map();
for (const sale of originalSales)
map.set(sale.id, sale);
const promises = [];
for (const sale of sales) {
const originalSale = map.get(sale.id);
if (sale.quantity == originalSale.quantity) {
const updatedSale = models.Sale.updateAll({
id: sale.id
}, {ticketFk: ticketId}, options);
promises.push(updatedSale);
} else if (sale.quantity < originalSale.quantity) {
const transferedSale = await transferPartialSale(
ticketId, originalSale, sale, options);
promises.push(transferedSale);
}
}
const isTicketEmpty = await models.Ticket.isEmpty(id, options);
if (isTicketEmpty) {
originalTicket.updateAttributes({
isDeleted: true
}, options);
}
await Promise.all(promises);
await tx.commit();
return {id: ticketId};
} catch (error) {
await tx.rollback();
throw error;
}
};
async function cloneTicket(ctx, ticket, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const travelDates = await models.Agency.getFirstShipped({
agencyModeFk: ticket.agencyModeFk,
addressFk: ticket.addressFk,
warehouseFk: ticket.warehouseFk
});
if (!travelDates)
throw new UserError(`Invalid parameters to create a new ticket`);
let shipped = new Date(travelDates.shipped);
let landed = new Date(travelDates.landed);
const newTicket = await models.Ticket.new(ctx, {
clientFk: ticket.clientFk,
addressFk: ticket.addressFk,
agencyModeFk: ticket.agencyModeFk,
warehouseFk: ticket.warehouseFk,
shipped: shipped,
landed: landed,
userId: userId
}, options);
return newTicket.id;
}
async function transferPartialSale(ticketId, originalSale, sale, options) {
const models = Self.app.models;
// Update original sale
const rest = originalSale.quantity - sale.quantity;
const updatedSale = models.Sale.updateAll({
id: sale.id
}, {quantity: rest}, options);
// Clone sale with new quantity
const newSale = originalSale;
newSale.id = undefined;
newSale.ticketFk = ticketId;
newSale.quantity = sale.quantity;
const createdSale = await models.Sale.create(newSale, options);
// Clone sale components
const saleComponents = await models.SaleComponent.find({
where: {saleFk: sale.id}
}, options);
const newComponents = saleComponents.map(component => {
component.saleFk = createdSale.id;
return component;
});
const createdComponents = models.SaleComponent
.create(newComponents, options);
return [updatedSale, createdComponents];
}
};

View File

@ -1,6 +1,5 @@
module.exports = Self => { module.exports = Self => {
require('../methods/sale/getClaimableFromTicket')(Self); require('../methods/sale/getClaimableFromTicket')(Self);
require('../methods/sale/moveToTicket')(Self);
require('../methods/sale/reserve')(Self); require('../methods/sale/reserve')(Self);
require('../methods/sale/removes')(Self); require('../methods/sale/removes')(Self);
require('../methods/sale/updatePrice')(Self); require('../methods/sale/updatePrice')(Self);

View File

@ -11,7 +11,6 @@ module.exports = Self => {
require('../methods/ticket/componentUpdate')(Self); require('../methods/ticket/componentUpdate')(Self);
require('../methods/ticket/new')(Self); require('../methods/ticket/new')(Self);
require('../methods/ticket/isEditable')(Self); require('../methods/ticket/isEditable')(Self);
require('../methods/ticket/threeLastActive')(Self);
require('../methods/ticket/delete')(Self); require('../methods/ticket/delete')(Self);
require('../methods/ticket/getVAT')(Self); require('../methods/ticket/getVAT')(Self);
require('../methods/ticket/getSales')(Self); require('../methods/ticket/getSales')(Self);
@ -21,10 +20,11 @@ module.exports = Self => {
require('../methods/ticket/canBeInvoiced')(Self); require('../methods/ticket/canBeInvoiced')(Self);
require('../methods/ticket/makeInvoice')(Self); require('../methods/ticket/makeInvoice')(Self);
require('../methods/ticket/updateEditableTicket')(Self); require('../methods/ticket/updateEditableTicket')(Self);
require('../methods/ticket/checkEmptiness')(Self); require('../methods/ticket/isEmpty')(Self);
require('../methods/ticket/updateDiscount')(Self); require('../methods/ticket/updateDiscount')(Self);
require('../methods/ticket/uploadFile')(Self); require('../methods/ticket/uploadFile')(Self);
require('../methods/ticket/addSale')(Self); require('../methods/ticket/addSale')(Self);
require('../methods/ticket/transferSales')(Self);
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) return; if (ctx.isNewInstance) return;

View File

@ -115,7 +115,11 @@
field="sale.itemFk" field="sale.itemFk"
show-field="name" show-field="name"
value-field="id" value-field="id"
search-function="{or: [{id: $search}, {name: {like: '%' + $search + '%'}}]}"> search-function="{or: [{id: $search}, {name: {like: '%' + $search + '%'}}]}"
order="id DESC">
<tpl-item>
{{id}} - {{name}}
</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</field> </field>
</vn-td-editable> </vn-td-editable>
@ -258,51 +262,87 @@
<!-- Transfer Popover --> <!-- Transfer Popover -->
<vn-popover class="transfer" vn-id="transfer"> <vn-popover class="transfer" vn-id="transfer">
<div pad-medium> <div pad-medium>
<table class="vn-grid">
<thead>
<tr>
<th number translate>ID</th>
<th number translate>F. envio</th>
<th number translate>Agencia</th>
<th number translate>Almacen</th>
</tr>
</thead>
<tbody>
<tr ng-if="$ctrl.lastThreeTickets.length === 0" >
<td colspan="4" style="text-align: center" translate>No results</td>
</tr>
<tr
class="clickable"
ng-repeat="ticket in $ctrl.lastThreeTickets track by ticket.id"
ng-click="$ctrl.checkEmptiness(ticket.id)">
<td number>{{::ticket.id}}</td>
<td number>{{::ticket.shipped | dateTime: 'dd/MM/yyyy HH:mm'}}</td>
<td number>{{::ticket.agencyName}}</td>
<td number>{{::ticket.warehouseName}}</td>
</tr>
</tbody>
</table>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-one>
label="Move to ticket" <h4 translate>Sales to transfer</h4>
model="$ctrl.receiverTicketId" <vn-table>
type="number"> <vn-thead>
</vn-textfield> <vn-tr>
<vn-icon-button <vn-th number shrink>Id</vn-th>
icon="arrow_forward_ios" <vn-th shrink>Item</vn-th>
ng-click="$ctrl.checkEmptiness($ctrl.receiverTicketId)"> <vn-th number shrink>Quantity</vn-th>
</vn-icon-button> </vn-tr>
</vn-horizontal> </vn-thead>
<vn-horizontal> <vn-tbody>
<vn-button <vn-tr ng-repeat="sale in $ctrl.transfer.sales">
label="New ticket" <vn-td number shrink>{{::sale.itemFk | zeroFill:6}}</vn-td>
ng-click="$ctrl.checkEmptiness()"> <vn-td shrink>
</vn-button> <span title="{{::sale.concept}}">{{::sale.concept}}</span>
<vn-icon </vn-td>
color-secondary <vn-td-editable number>
vn-tooltip="You have to allow pop-ups in your web browser to use this functionality" <text>{{sale.quantity}}</text>
icon="info"> <field>
</vn-icon> <vn-input-number vn-focus
model="sale.quantity">
</vn-input-number>
</field>
</vn-td-editable>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-one>
<vn-one>
<vn-horizontal>
<h4 vn-one translate>Destination ticket</h4>
<vn-icon vn-none
color-secondary
vn-tooltip="You have to allow pop-ups in your web browser to use this functionality"
icon="info">
</vn-icon>
</vn-horizontal>
<table class="vn-grid">
<thead>
<tr>
<th number>ID</th>
<th number>F. envio</th>
<th number>Agencia</th>
<th number>Almacen</th>
</tr>
</thead>
<tbody>
<tr ng-if="$ctrl.transfer.lastActiveTickets.length === 0" >
<td colspan="4" style="text-align: center" translate>No results</td>
</tr>
<tr
class="clickable"
ng-repeat="ticket in $ctrl.transfer.lastActiveTickets track by ticket.id"
ng-click="$ctrl.transferSales(ticket.id)">
<td number>{{::ticket.id}}</td>
<td number>{{::ticket.shipped | dateTime: 'dd/MM/yyyy'}}</td>
<td number>{{::ticket.agencyName}}</td>
<td number>{{::ticket.warehouseName}}</td>
</tr>
</tbody>
</table>
<vn-horizontal pad-medium-v>
<vn-textfield vn-one
label="Transfer to ticket"
model="$ctrl.transfer.ticketId"
type="number">
</vn-textfield>
<vn-icon-button vn-none
icon="arrow_forward_ios"
ng-click="$ctrl.transferSales($ctrl.transfer.ticketId)">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal pad-medium-v>
<vn-button
label="New ticket"
ng-click="$ctrl.transferSales()">
</vn-button>
</vn-horizontal>
</vn-one>
</vn-horizontal> </vn-horizontal>
</div> </div>
</vn-popover> </vn-popover>
@ -321,7 +361,7 @@
vn-id="delete-ticket" vn-id="delete-ticket"
question="Do you want to delete it?" question="Do you want to delete it?"
message="This ticket is now empty" message="This ticket is now empty"
on-response="$ctrl.moveLines(response)"> on-response="$ctrl.transferSales($ctrl.transfer.ticketId, response)">
</vn-confirm> </vn-confirm>
<vn-float-button <vn-float-button
ng-show="$ctrl.isEditable" ng-show="$ctrl.isEditable"

View File

@ -99,6 +99,7 @@ class Controller {
this.isEditable = res.data; this.isEditable = res.data;
}); });
} }
get isChecked() { get isChecked() {
if (this.sales) { if (this.sales) {
for (let instance of this.sales) for (let instance of this.sales)
@ -113,7 +114,7 @@ class Controller {
* *
* @return {Array} Checked instances * @return {Array} Checked instances
*/ */
getCheckedLines() { checkedLines() {
if (!this.sales) return; if (!this.sales) return;
return this.sales.filter(sale => { return this.sales.filter(sale => {
@ -121,13 +122,14 @@ class Controller {
}); });
} }
/** /**
* Returns an array of indexes * Returns an array of indexes
* from checked instances * from checked instances
* *
* @return {Array} Indexes of checked instances * @return {Array} Indexes of checked instances
*/ */
getCheckedLinesIndex() { checkedLinesIndex() {
if (!this.sales) return; if (!this.sales) return;
let indexes = []; let indexes = [];
@ -138,8 +140,25 @@ class Controller {
return indexes; return indexes;
} }
firstCheckedLine() {
const checkedLines = this.checkedLines();
if (checkedLines)
return checkedLines[0];
}
/**
* Returns the total of checked instances
*
* @return {Number} Total checked instances
*/
totalCheckedLines() {
const checkedLines = this.checkedLines();
if (checkedLines)
return checkedLines.length;
}
removeCheckedLines() { removeCheckedLines() {
const sales = this.getCheckedLines(); const sales = this.checkedLines();
sales.forEach(sale => { sales.forEach(sale => {
const index = this.sales.indexOf(sale); const index = this.sales.indexOf(sale);
@ -167,7 +186,7 @@ class Controller {
onRemoveLinesClick(response) { onRemoveLinesClick(response) {
if (response === 'ACCEPT') { if (response === 'ACCEPT') {
let sales = this.getCheckedLines(); let sales = this.checkedLines();
// Remove unsaved instances // Remove unsaved instances
sales.forEach((sale, index) => { sales.forEach((sale, index) => {
@ -188,60 +207,36 @@ class Controller {
} }
showTransferPopover(event) { showTransferPopover(event) {
let filter = {clientFk: this.ticket.clientFk, ticketFk: this.ticket.id}; this.setTransferParams();
let json = encodeURIComponent(JSON.stringify(filter));
let query = `/api/Tickets/threeLastActive?filter=${json}`;
this.$http.get(query).then(res => {
this.lastThreeTickets = res.data;
});
this.$scope.transfer.parent = event.target; this.$scope.transfer.parent = event.target;
this.$scope.transfer.show(); this.$scope.transfer.show();
} }
setTransferParams() {
const checkedSales = JSON.stringify(this.checkedLines());
const sales = JSON.parse(checkedSales);
this.transfer = {
lastActiveTickets: [],
sales: sales
};
checkEmptiness(receiverTicketId) { const params = {ticketId: this.ticket.id};
let sales = this.getCheckedLines(); const query = `/api/clients/${this.ticket.clientFk}/lastActiveTickets`;
let areAllSalesSelected = sales.length === this.$scope.model.data.length; this.$http.get(query, {params}).then(res => {
this.receiverTicketId = receiverTicketId; this.transfer.lastActiveTickets = res.data;
});
if (areAllSalesSelected) {
let query = `/api/Tickets/${this.ticket.id}/checkEmptiness`;
this.$http.get(query).then(res => {
if (res.data)
this.$scope.deleteTicket.show();
if (!res.data)
this.moveLines(false);
});
}
if (!areAllSalesSelected)
this.moveLines(false);
} }
moveLines(removeEmptyTicket) { transferSales(ticketId) {
let sales = this.getCheckedLines(); const params = {
ticketId: ticketId,
let currentTicketData = { sales: this.transfer.sales
currentTicketId: this.ticket.id,
clientFk: this.ticket.clientFk,
addressFk: this.ticket.addressFk,
agencyModeFk: this.ticket.agencyModeFk,
warehouseFk: this.ticket.warehouseFk
}; };
let params = { const query = `/api/tickets/${this.ticket.id}/transferSales`;
currentTicket: currentTicketData, this.$http.post(query, params).then(res => {
receiverTicket: this.receiverTicketId ? {id: this.receiverTicketId} : currentTicketData, if (res.data)
sales: sales,
removeEmptyTicket: removeEmptyTicket
};
this.$http.post(`/api/Sales/moveToTicket`, params).then(res => {
if (res.data) {
this.receiverTicketId = null;
this.goToTicket(res.data.id); this.goToTicket(res.data.id);
}
}); });
} }
@ -251,14 +246,14 @@ class Controller {
clientFk: this.ticket.clientFk, clientFk: this.ticket.clientFk,
ticketCreated: this.ticket.shipped ticketCreated: this.ticket.shipped
}; };
const sales = this.getCheckedLines(); const sales = this.checkedLines();
this.$http.post(`/api/Claims/createFromSales`, {claim: claim, sales: sales}).then(res => { this.$http.post(`/api/Claims/createFromSales`, {claim: claim, sales: sales}).then(res => {
this.$state.go('claim.card.basicData', {id: res.data.id}); this.$state.go('claim.card.basicData', {id: res.data.id});
}); });
} }
goToTicket(ticketID) { goToTicket(ticketId) {
this.$state.go('ticket.card.sale', {id: ticketID}); this.$state.go('ticket.card.sale', {id: ticketId});
} }
// Slesperson Mana // Slesperson Mana
@ -336,7 +331,7 @@ class Controller {
} }
showEditDialog() { showEditDialog() {
this.edit = this.getCheckedLines(); this.edit = this.checkedLines();
this.$scope.editDialog.show(); this.$scope.editDialog.show();
} }
@ -365,7 +360,7 @@ class Controller {
} }
setReserved(reserved) { setReserved(reserved) {
let selectedSales = this.getCheckedLines(); let selectedSales = this.checkedLines();
let params = {sales: selectedSales, ticketFk: this.ticket.id, reserved: reserved}; let params = {sales: selectedSales, ticketFk: this.ticket.id, reserved: reserved};
let reservedSales = new Map(); let reservedSales = new Map();
@ -393,7 +388,7 @@ class Controller {
showSMSDialog() { showSMSDialog() {
const address = this.ticket.address; const address = this.ticket.address;
const sales = this.getCheckedLines(); const sales = this.checkedLines();
const items = sales.map(sale => { const items = sales.map(sale => {
return `${sale.quantity} ${sale.concept}`; return `${sale.quantity} ${sale.concept}`;
}); });

View File

@ -7,7 +7,7 @@ Unmark as reserved: Desmarcar como reservado
Update discount: Actualizar descuento Update discount: Actualizar descuento
There is no changes to save: No hay cambios que guardar There is no changes to save: No hay cambios que guardar
Edit discount: Editar descuento Edit discount: Editar descuento
Move to ticket: Mover a ticket Transfer to ticket: Transferir a ticket
New ticket: Nuevo ticket New ticket: Nuevo ticket
Edit price: Editar precio Edit price: Editar precio
You are going to delete lines of the ticket: Vas a borrar lineas del ticket You are going to delete lines of the ticket: Vas a borrar lineas del ticket
@ -19,7 +19,8 @@ Available: Disponible
In which day you want to add the ticket?: ¿A que dia quieres añadir el ticket? In which day you want to add the ticket?: ¿A que dia quieres añadir el ticket?
Add claim: Crear reclamación Add claim: Crear reclamación
Claim: Reclamación Claim: Reclamación
Transfer lines: Transferir líneas Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
Change ticket state to 'Ok': Cambiar estado del ticket a 'Ok' Change ticket state to 'Ok': Cambiar estado del ticket a 'Ok'
Reserved: Reservado Reserved: Reservado
SMSAvailability: >- SMSAvailability: >-

View File

@ -9,7 +9,7 @@ describe('Ticket', () => {
let ticket = { let ticket = {
id: 1, id: 1,
clientFk: 1, clientFk: 101,
shipped: 1, shipped: 1,
created: new Date(), created: new Date(),
client: {salesPersonFk: 1}, client: {salesPersonFk: 1},
@ -92,13 +92,13 @@ describe('Ticket', () => {
}); });
}); });
describe('getCheckedLines()', () => { describe('checkedLines()', () => {
it('should make an array of the instances with the property checked true()', () => { it('should make an array of the instances with the property checked true()', () => {
let sale = controller.sales[1]; let sale = controller.sales[1];
sale.checked = true; sale.checked = true;
let expectedResult = [sale]; let expectedResult = [sale];
expect(controller.getCheckedLines()).toEqual(expectedResult); expect(controller.checkedLines()).toEqual(expectedResult);
}); });
}); });
@ -235,5 +235,46 @@ describe('Ticket', () => {
$httpBackend.flush(); $httpBackend.flush();
}); });
}); });
describe('transferSales()', () => {
it('should transfer sales to a ticket', () => {
spyOn(controller, 'goToTicket');
controller.transfer = {
sales: [{id: 1, itemFk: 1}, {id: 2, itemFk: 4}]
};
const expectedResponse = {id: 13};
const params = {
ticketId: 13,
sales: controller.transfer.sales
};
$httpBackend.when('POST', `/api/tickets/1/transferSales`, params).respond(expectedResponse);
$httpBackend.expect('POST', `/api/tickets/1/transferSales`, params).respond(expectedResponse);
controller.transferSales(13);
$httpBackend.flush();
expect(controller.goToTicket).toHaveBeenCalledWith(13);
});
});
describe('setTransferParams()', () => {
it('should define the transfer object on the controller and its default properties', () => {
let sale = controller.sales[1];
sale.checked = true;
const expectedResponse = [sale];
$httpBackend.when('GET', `/api/clients/101/lastActiveTickets?ticketId=1`).respond(expectedResponse);
$httpBackend.expect('GET', `/api/clients/101/lastActiveTickets?ticketId=1`).respond(expectedResponse);
controller.setTransferParams();
$httpBackend.flush();
const lastActiveTickets = controller.transfer.lastActiveTickets;
expect(controller.transfer).toBeDefined();
expect(lastActiveTickets).toBeDefined();
expect(lastActiveTickets[0].id).toEqual(4);
});
});
}); });
}); });

View File

@ -61,13 +61,30 @@ vn-ticket-sale {
} }
vn-popover.transfer{ vn-popover.transfer{
vn-table { vn-textfield {
min-width: 650px; margin: 0
}
vn-horizontal {
& > vn-one:nth-child(1){
border-right: 1px solid $color-bg;
padding-right: 1em;
}
& > vn-one:nth-child(2){
margin-left: 1em
}
}
vn-table, table {
margin-bottom: 10px; margin-bottom: 10px;
} }
vn-icon:nth-child(1) {
padding-top: 0.2em; vn-table {
font-size: 1.7em; width: 20em
}
table {
width: 25em
} }
} }