Merge pull request '#7710 - clone with ticket packaging' (!2878) from 7710-cloneWithTicketPackaging into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #2878
Reviewed-by: Javier Segarra <jsegarra@verdnatura.es>
This commit is contained in:
Javi Gallego 2024-08-23 13:41:39 +00:00
commit 638a43d3e6
16 changed files with 211 additions and 281 deletions

View File

@ -0,0 +1,24 @@
DELETE FROM `salix`.`ACL`
WHERE `model` = 'Ticket'
AND `property` = 'refund'
AND `accessType` = 'WRITE'
AND `permission` = 'ALLOW'
AND `principalType` = 'ROLE'
AND `principalId` = 'salesAssistant';
UPDATE `salix`.`ACL`
SET `property` = 'cloneAll'
WHERE `model` = 'Ticket'
AND `property` = 'refund'
AND `accessType` = 'WRITE'
AND `permission` = 'ALLOW'
AND `principalType` = 'ROLE'
AND `principalId` IN ('invoicing', 'claimManager', 'logistic');
DELETE FROM `salix`.`ACL`
WHERE `model` = 'Ticket'
AND `property` = 'clone'
AND `accessType` = 'WRITE'
AND `permission` = 'ALLOW'
AND `principalType` = 'ROLE'
AND `principalId` = 'administrative';

View File

@ -43,7 +43,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions); const tickets = await models.Ticket.find(filter, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id); const ticketsIds = tickets.map(ticket => ticket.id);
const refundedTickets = await models.Ticket.refund(ctx, ticketsIds, withWarehouse, myOptions); const refundedTickets = await models.Ticket.cloneAll(ctx, ticketsIds, withWarehouse, true, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -82,20 +82,12 @@ module.exports = Self => {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
try { try {
const filterRef = {where: {refFk: refFk}}; const tickets = await models.Ticket.find({where: {refFk}}, myOptions);
const tickets = await models.Ticket.find(filterRef, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id); const ticketsIds = tickets.map(ticket => ticket.id);
const refundTickets = await models.Ticket.refund(ctx, ticketsIds, null, myOptions); const refundTickets = await models.Ticket.cloneAll(ctx, ticketsIds, false, true, myOptions);
const filterTicket = {where: {ticketFk: {inq: ticketsIds}}}; const clonedTickets = await models.Ticket.cloneAll(ctx, ticketsIds, false, false, myOptions);
const services = await models.TicketService.find(filterTicket, myOptions);
const servicesIds = services.map(service => service.id);
const sales = await models.Sale.find(filterTicket, myOptions);
const salesIds = sales.map(sale => sale.id);
const clonedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, myOptions);
const clonedTicketIds = []; const clonedTicketIds = [];
for (const clonedTicket of clonedTickets) { for (const clonedTicket of clonedTickets) {

View File

@ -88,28 +88,7 @@
translate> translate>
Show CITES letter Show CITES letter
</vn-item> </vn-item>
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-tooltip="Create a refund ticket for each ticket on the current invoice"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refundInvoiceOut(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refundInvoiceOut(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-list> </vn-list>
</vn-menu> </vn-menu>
<vn-confirm <vn-confirm

View File

@ -135,21 +135,6 @@ class Controller extends Section {
}); });
} }
refundInvoiceOut(withWarehouse) {
const query = 'InvoiceOuts/refund';
const params = {ref: this.invoiceOut.ref, withWarehouse: withWarehouse};
this.$http.post(query, params).then(res => {
const tickets = res.data;
const refundTickets = tickets.map(ticket => ticket.id);
this.vnApp.showSuccess(this.$t('The following refund tickets have been created', {
ticketId: refundTickets.join(',')
}));
if (refundTickets.length == 1)
this.$state.go('ticket.card.sale', {id: refundTickets[0]});
});
}
transferInvoice() { transferInvoice() {
const params = { const params = {
id: this.invoiceOut.id, id: this.invoiceOut.id,

View File

@ -105,17 +105,4 @@ describe('vnInvoiceOutDescriptorMenu', () => {
expect(controller.vnApp.showMessage).toHaveBeenCalled(); expect(controller.vnApp.showMessage).toHaveBeenCalled();
}); });
}); });
describe('refundInvoiceOut()', () => {
it('should make a query and show a success message', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const params = {ref: controller.invoiceOut.ref};
$httpBackend.expectPOST(`InvoiceOuts/refund`, params).respond([{id: 1}, {id: 2}]);
controller.refundInvoiceOut();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
}); });

View File

@ -1,40 +1,25 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('clone', { Self.remoteMethodCtx('clone', {
description: 'Clone sales and services provided', description: 'Clone sales, services, and ticket packaging provided',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [ accepts: [
{ {arg: 'salesIds', type: ['number']},
arg: 'salesIds', {arg: 'servicesIds', type: ['number']},
type: ['number'], {arg: 'ticketPackagingIds', type: ['number']},
}, { {arg: 'withWarehouse', type: 'boolean', required: true},
arg: 'servicesIds', {arg: 'negative', type: 'boolean'}
type: ['number']
}, {
arg: 'withWarehouse',
type: 'boolean',
required: true
}, {
arg: 'negative',
type: 'boolean'
}
], ],
returns: { returns: {type: ['object'], root: true},
type: ['object'], http: {path: `/clone`, verb: 'POST'}
root: true
},
http: {
path: `/clone`,
verb: 'POST'
}
}); });
Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, negative, options) => {
Self.clone = async(ctx, salesIds, servicesIds, ticketPackagingIds, withWarehouse, negative, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
const newTickets = []; const newTickets = [];
if (typeof options == 'object') if (typeof options === 'object') Object.assign(myOptions, options);
Object.assign(myOptions, options);
if (!myOptions.transaction) { if (!myOptions.transaction) {
tx = await Self.beginTransaction({}); tx = await Self.beginTransaction({});
@ -44,8 +29,9 @@ module.exports = Self => {
try { try {
let sales; let sales;
let services; let services;
let ticketPackaging;
if (salesIds && salesIds.length) { if (salesIds?.length) {
sales = await models.Sale.find({ sales = await models.Sale.find({
where: {id: {inq: salesIds}}, where: {id: {inq: salesIds}},
include: { include: {
@ -57,12 +43,18 @@ module.exports = Self => {
}, myOptions); }, myOptions);
} }
if (servicesIds && servicesIds.length) { if (servicesIds?.length) {
services = await models.TicketService.find({ services = await models.TicketService.find({
where: {id: {inq: servicesIds}} where: {id: {inq: servicesIds}}
}, myOptions); }, myOptions);
} }
if (ticketPackagingIds?.length) {
ticketPackaging = await models.TicketPackaging.find({
where: {id: {inq: ticketPackagingIds}}
}, myOptions);
}
let ticketsIds = sales ? let ticketsIds = sales ?
[...new Set(sales.map(sale => sale.ticketFk))] : [...new Set(sales.map(sale => sale.ticketFk))] :
[...new Set(services.map(service => service.ticketFk))]; [...new Set(services.map(service => service.ticketFk))];
@ -74,12 +66,12 @@ module.exports = Self => {
ctx, ctx,
ticketId, ticketId,
withWarehouse, withWarehouse,
negative,
myOptions myOptions
); );
newTickets.push(newTicket); newTickets.push(newTicket);
mappedTickets.set(ticketId, newTicket.id); mappedTickets.set(ticketId, newTicket.id);
} }
if (sales) { if (sales) {
for (const sale of sales) { for (const sale of sales) {
const newTicketId = mappedTickets.get(sale.ticketFk); const newTicketId = mappedTickets.get(sale.ticketFk);
@ -116,6 +108,18 @@ module.exports = Self => {
} }
} }
if (ticketPackaging) {
for (const packaging of ticketPackaging) {
const newTicketId = mappedTickets.get(packaging.ticketFk);
await models.TicketPackaging.create({
ticketFk: newTicketId,
packagingFk: packaging.packagingFk,
quantity: negative ? -packaging.quantity : packaging.quantity
}, myOptions);
}
}
if (tx) await tx.commit(); if (tx) await tx.commit();
return newTickets; return newTickets;
@ -124,13 +128,7 @@ module.exports = Self => {
throw e; throw e;
} }
async function createTicket( async function createTicket(ctx, ticketId, withWarehouse, myOptions) {
ctx,
ticketId,
withWarehouse,
negative,
myOptions
) {
const models = Self.app.models; const models = Self.app.models;
const now = Date.vnNew(); const now = Date.vnNew();

View File

@ -20,7 +20,7 @@ describe('Ticket cloning - clone function', () => {
const servicesIds = []; const servicesIds = [];
const withWarehouse = true; const withWarehouse = true;
const negative = false; const negative = false;
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, withWarehouse, negative, options); const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, withWarehouse, negative, options);
expect(newTickets).toBeDefined(); expect(newTickets).toBeDefined();
expect(newTickets.length).toBeGreaterThan(0); expect(newTickets.length).toBeGreaterThan(0);
@ -30,7 +30,7 @@ describe('Ticket cloning - clone function', () => {
const negative = true; const negative = true;
const salesIds = [7, 8]; const salesIds = [7, 8];
const servicesIds = []; const servicesIds = [];
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, false, negative, options); const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, negative, options);
for (const ticket of newTickets) { for (const ticket of newTickets) {
const sales = await models.Sale.find({where: {ticketFk: ticket.id}}, options); const sales = await models.Sale.find({where: {ticketFk: ticket.id}}, options);
@ -43,7 +43,7 @@ describe('Ticket cloning - clone function', () => {
it('should create new components and services for cloned tickets', async() => { it('should create new components and services for cloned tickets', async() => {
const servicesIds = [2]; const servicesIds = [2];
const salesIds = [5]; const salesIds = [5];
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, false, false, options); const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, false, options);
for (const ticket of newTickets) { for (const ticket of newTickets) {
const sale = await models.Sale.findOne({where: {ticketFk: ticket.id}}, options); const sale = await models.Sale.findOne({where: {ticketFk: ticket.id}}, options);
@ -58,7 +58,7 @@ describe('Ticket cloning - clone function', () => {
it('should create a ticket without sales', async() => { it('should create a ticket without sales', async() => {
const servicesIds = [4]; const servicesIds = [4];
const tickets = await models.Sale.clone(ctx, null, servicesIds, false, false, options); const tickets = await models.Sale.clone(ctx, null, servicesIds, null, false, false, options);
const refundedTicket = await getTicketRefund(tickets[0].id, options); const refundedTicket = await getTicketRefund(tickets[0].id, options);
expect(refundedTicket).toBeDefined(); expect(refundedTicket).toBeDefined();

View File

@ -1,54 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('clone', {
description: 'clone a ticket and return the new ticket id',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The ticket id',
http: {source: 'path'}
}, {
arg: 'shipped',
type: 'date',
}, {
arg: 'withWarehouse',
type: 'boolean',
}
],
returns: {
type: 'number',
root: true
},
http: {
path: `/:id/clone`,
verb: 'POST'
}
});
Self.clone = async(ctx, id, shipped, withWarehouse, options) => {
const myOptions = {userId: ctx.req.accessToken.userId};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const [, [{clonedTicketId}]] = await Self.rawSql(`
CALL vn.ticket_cloneAll(?, ?, ?, @clonedTicketId);
SELECT @clonedTicketId clonedTicketId;`,
[id, shipped, withWarehouse], myOptions);
if (tx) await tx.commit();
return clonedTicketId;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,77 @@
module.exports = Self => {
Self.remoteMethodCtx('cloneAll', {
description: 'Clone tickets, sales, services and packages',
accessType: 'WRITE',
accepts: [
{
arg: 'ticketsIds',
type: ['number'],
required: true,
description: 'IDs of the tickets to clone'
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true,
description: 'true: keep original warehouse; false: set to null'
},
{
arg: 'negative',
type: 'boolean',
required: true,
description: 'true: invert quantities; false: keep as is.'
}
],
returns: {
type: ['object'],
root: true,
description: 'The cloned tickets with associated data'
},
http: {
path: `/cloneAll`,
verb: 'POST'
}
});
Self.cloneAll = async(ctx, ticketsIds, withWarehouse, negative, options) => {
const models = Self.app.models;
const myOptions = typeof options == 'object' ? {...options} : {};
let tx;
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const filter = {where: {ticketFk: {inq: ticketsIds}}};
const [sales, services, ticketPackaging] = await Promise.all([
models.Sale.find(filter, myOptions),
models.TicketService.find(filter, myOptions),
models.TicketPackaging.find(filter, myOptions)
]);
const salesIds = sales.map(({id}) => id);
const servicesIds = services.map(({id}) => id);
const ticketPackagingIds = ticketPackaging.map(({id}) => id);
const clonedTickets = await models.Sale.clone(
ctx,
salesIds,
servicesIds,
ticketPackagingIds,
withWarehouse,
negative,
myOptions
);
if (tx) await tx.commit();
return clonedTickets;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -1,58 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('refund', {
description: 'Create refund tickets with all their sales and services',
accessType: 'WRITE',
accepts: [
{
arg: 'ticketsIds',
type: ['number'],
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/refund`,
verb: 'POST'
}
});
Self.refund = async(ctx, ticketsIds, withWarehouse, 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 filter = {where: {ticketFk: {inq: ticketsIds}}};
const sales = await models.Sale.find(filter, myOptions);
const salesIds = sales.map(sale => sale.id);
const services = await models.TicketService.find(filter, myOptions);
const servicesIds = services.map(service => service.id);
const refundedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, withWarehouse, true, myOptions);
if (tx) await tx.commit();
return refundedTickets;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -1,43 +0,0 @@
const models = require('vn-loopback/server/server').models;
describe('Ticket cloning - clone function', () => {
const ctx = beforeAll.getCtx();
let options;
let tx;
const ticketId = 1;
const shipped = Date.vnNew();
beforeEach(async() => {
options = {transaction: tx};
tx = await models.Ticket.beginTransaction({});
options.transaction = tx;
});
afterEach(async() => {
await tx.rollback();
});
it('should clone a new ticket without warehouse', async() => {
const originalTicket = await models.Ticket.findById(ticketId, null, options);
const newTicketId = await models.Ticket.clone(ctx, ticketId, shipped, false, options);
const newTicket = await models.Ticket.findById(newTicketId, null, options);
expect(newTicket.clientFk).toEqual(originalTicket.clientFk);
expect(newTicket.companyFk).toEqual(originalTicket.companyFk);
expect(newTicket.addressFk).toEqual(originalTicket.addressFk);
expect(newTicket.warehouseFk).toBeFalsy();
});
it('should clone a new ticket with warehouse', async() => {
const originalTicket = await models.Ticket.findById(ticketId, null, options);
const newTicketId = await models.Ticket.clone(ctx, ticketId, shipped, true, options);
const newTicket = await models.Ticket.findById(newTicketId, null, options);
expect(newTicket.clientFk).toEqual(originalTicket.clientFk);
expect(newTicket.companyFk).toEqual(originalTicket.companyFk);
expect(newTicket.addressFk).toEqual(originalTicket.addressFk);
expect(newTicket.warehouseFk).toEqual(originalTicket.warehouseFk);
});
});

View File

@ -0,0 +1,53 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Ticket cloning - cloneAll function', () => {
const activeCtx = {
accessToken: {userId: 1},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
let options;
let tx;
const ticketIds = [1, 2];
const withWarehouse = true;
const negative = false;
beforeEach(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: ctx.req});
tx = await models.Ticket.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
if (tx)
await tx.rollback();
});
it('should clone all provided tickets with their associated sales, services, and packages', async() => {
const originalTickets = await models.Ticket.find({where: {id: {inq: ticketIds}}}, options);
const originalSales = await models.Sale.find({where: {ticketFk: {inq: ticketIds}}}, options);
const originalServices = await models.TicketService.find({where: {ticketFk: {inq: ticketIds}}}, options);
const originalTicketPackaging =
await models.TicketPackaging.find({where: {ticketFk: {inq: ticketIds}}}, options);
// Pass the ctx correctly to the cloneAll function
const clonedTickets = await models.Ticket.cloneAll(ctx, ticketIds, withWarehouse, negative, options);
expect(clonedTickets.length).toEqual(originalTickets.length);
const clonedSales = await models.Sale.find({where: {ticketFk: {inq: clonedTickets.map(t => t.id)}}}, options);
const clonedServices =
await models.TicketService.find({where: {ticketFk: {inq: clonedTickets.map(t => t.id)}}}, options);
const clonedTicketPackaging =
await models.TicketPackaging.find({where: {ticketFk: {inq: clonedTickets.map(t => t.id)}}}, options);
expect(clonedSales.length).toEqual(originalSales.length);
expect(clonedServices.length).toEqual(originalServices.length);
expect(clonedTicketPackaging.length).toEqual(originalTicketPackaging.length);
});
});

View File

@ -26,7 +26,7 @@ module.exports = function(Self) {
require('../methods/ticket/isLocked')(Self); require('../methods/ticket/isLocked')(Self);
require('../methods/ticket/freightCost')(Self); require('../methods/ticket/freightCost')(Self);
require('../methods/ticket/getComponentsSum')(Self); require('../methods/ticket/getComponentsSum')(Self);
require('../methods/ticket/refund')(Self); require('../methods/ticket/cloneAll')(Self);
require('../methods/ticket/deliveryNotePdf')(Self); require('../methods/ticket/deliveryNotePdf')(Self);
require('../methods/ticket/deliveryNoteEmail')(Self); require('../methods/ticket/deliveryNoteEmail')(Self);
require('../methods/ticket/deliveryNoteCsv')(Self); require('../methods/ticket/deliveryNoteCsv')(Self);
@ -46,5 +46,4 @@ module.exports = function(Self) {
require('../methods/ticket/invoiceTicketsAndPdf')(Self); require('../methods/ticket/invoiceTicketsAndPdf')(Self);
require('../methods/ticket/docuwareDownload')(Self); require('../methods/ticket/docuwareDownload')(Self);
require('../methods/ticket/myLastModified')(Self); require('../methods/ticket/myLastModified')(Self);
require('../methods/ticket/clone')(Self);
}; };

View File

@ -287,15 +287,24 @@ class Controller extends Section {
} }
refund(withWarehouse) { refund(withWarehouse) {
const params = {ticketsIds: [this.id], withWarehouse: withWarehouse}; const params = {
const query = 'Tickets/refund'; ticketsIds: [this.id],
withWarehouse: withWarehouse,
negative: true // Asumimos que queremos cantidades negativas para reembolsos
};
const query = 'Tickets/cloneAll';
return this.$http.post(query, params) return this.$http.post(query, params)
.then(res => { .then(res => {
const [refundTicket] = res.data; const [refundTicket] = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket has been created', {
ticketId: refundTicket.id ticketId: refundTicket.id
})); }));
this.$state.go('ticket.card.sale', {id: refundTicket.id}); this.$state.go('ticket.card.sale', {id: refundTicket.id});
})
.catch(error => {
this.vnApp.showError(this.$t('Error creating refund ticket', {
error: error.data?.error?.message || 'Unknown error'
}));
}); });
} }

View File

@ -217,24 +217,6 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
}); });
}); });
describe('refund()', () => {
it('should make a query and go to ticket.card.sale', () => {
controller.$state.go = jest.fn();
controller._id = ticket.id;
const params = {
ticketsIds: [16]
};
const response = {id: 99};
$httpBackend.expectPOST('Tickets/refund', params).respond([response]);
controller.refund();
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', response);
});
});
describe('sendChangesSms()', () => { describe('sendChangesSms()', () => {
it('should make a query and open the sms dialog', () => { it('should make a query and open the sms dialog', () => {
controller.$.sms = {open: () => {}}; controller.$.sms = {open: () => {}};