#7710 - clone with ticket packaging #2878

Merged
jgallego merged 2 commits from 7710-cloneWithTicketPackaging into dev 2024-08-23 13:41:40 +00:00
16 changed files with 211 additions and 281 deletions

View File

@ -0,0 +1,24 @@
DELETE FROM `salix`.`ACL`
Review

Entiendo que con estos DELETES quitamos permisos a 2 roles

Entiendo que con estos DELETES quitamos permisos a 2 roles
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 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();

View File

@ -82,20 +82,12 @@ module.exports = Self => {
myOptions.transaction = tx;
}
try {
const filterRef = {where: {refFk: refFk}};
const tickets = await models.Ticket.find(filterRef, myOptions);
const tickets = await models.Ticket.find({where: {refFk}}, myOptions);

Si la clave tiene el mismo nombre que la variable del valor, basta con hacer {where:{refFk}

Si la clave tiene el mismo nombre que la variable del valor, basta con hacer `{where:{refFk}`
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 = [];
for (const clonedTicket of clonedTickets) {

View File

@ -88,28 +88,7 @@
translate>
Show CITES letter
</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-menu>
<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() {
const params = {
id: this.invoiceOut.id,

View File

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

View File

@ -20,7 +20,7 @@ describe('Ticket cloning - clone function', () => {
const servicesIds = [];
const withWarehouse = true;
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.length).toBeGreaterThan(0);
@ -30,7 +30,7 @@ describe('Ticket cloning - clone function', () => {
const negative = true;
const salesIds = [7, 8];
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) {
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() => {
const servicesIds = [2];
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) {
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() => {
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);
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.'

Siguiendo con el patrón del campo anterior.
Se asume que si es true: Whether...

Siguiendo con el patrón del campo anterior. Se asume que si es true: Whether...
}
],
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)
Review

Si una promesa falla...la siguientes fallaran.
Propuesta, usar allSettled

Si una promesa falla...la siguientes fallaran. Propuesta, usar allSettled
Review

No lo sabia, he preguntado a chatgpt, las diferencias, no tengo claro que en este caso nos aporte algo, PERO decidelo tu leyendo esto que tendras mas criterio

Promise.all puede ser adecuado si deseas abortar todo el proceso tan pronto como una promesa falle. Esto puede ser útil para asegurarte de que, si hay un error temprano, no continúas con acciones que dependen de ese estado.

Promise.allSettled sería útil si prefieres recopilar los resultados o errores de todas las promesas y luego decidir cómo proceder. Esto es útil cuando fallas en una parte del proceso no deben impedirte completar otras partes.

No lo sabia, he preguntado a chatgpt, las diferencias, no tengo claro que en este caso nos aporte algo, PERO decidelo tu leyendo esto que tendras mas criterio > Promise.all puede ser adecuado si deseas abortar todo el proceso tan pronto como una promesa falle. Esto puede ser útil para asegurarte de que, si hay un error temprano, no continúas con acciones que dependen de ese estado. > Promise.allSettled sería útil si prefieres recopilar los resultados o errores de todas las promesas y luego decidir cómo proceder. Esto es útil cuando fallas en una parte del proceso no deben impedirte completar otras partes.
Review

en realidad queremos que si una falle el resto también.
Lo había planteado al revés. dejamos Promises.all

en realidad queremos que si una falle el resto también. Lo había planteado al revés. dejamos Promises.all
]);
const salesIds = sales.map(({id}) => id);
const servicesIds = services.map(({id}) => id);
const ticketPackagingIds = ticketPackaging.map(({id}) => id);
Review

Propuesta:
const salesIds = sales.map(({id})=> id);

Y podria servir para las otras 2 más

O incluso en la linea 52 en vez de hacer [sales, services, ticketPackaging] podrías usar ese map para cada elemento del array

Por ejemplo:

` const results = await Promise.allSettled([
models.Sale.find(filter, myOptions),
models.TicketService.find(filter, myOptions),
models.TicketPackaging.find(filter, myOptions)
]);

const [salesIds, servicesIds, ticketPackagingIds] = results.map(result =>
    result.value.map(({ id }) => id) 
);`
Propuesta: `const salesIds = sales.map(({id})=> id);` Y podria servir para las otras 2 más O incluso en la linea 52 en vez de hacer `[sales, services, ticketPackaging]` podrías usar ese map para cada elemento del array Por ejemplo: ` const results = await Promise.allSettled([ models.Sale.find(filter, myOptions), models.TicketService.find(filter, myOptions), models.TicketPackaging.find(filter, myOptions) ]); const [salesIds, servicesIds, ticketPackagingIds] = results.map(result => result.value.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/freightCost')(Self);
require('../methods/ticket/getComponentsSum')(Self);
require('../methods/ticket/refund')(Self);
require('../methods/ticket/cloneAll')(Self);
require('../methods/ticket/deliveryNotePdf')(Self);
require('../methods/ticket/deliveryNoteEmail')(Self);
require('../methods/ticket/deliveryNoteCsv')(Self);
@ -46,5 +46,4 @@ module.exports = function(Self) {
require('../methods/ticket/invoiceTicketsAndPdf')(Self);
require('../methods/ticket/docuwareDownload')(Self);
require('../methods/ticket/myLastModified')(Self);
require('../methods/ticket/clone')(Self);
};

View File

@ -287,15 +287,24 @@ class Controller extends Section {
}
refund(withWarehouse) {
const params = {ticketsIds: [this.id], withWarehouse: withWarehouse};
const query = 'Tickets/refund';
const params = {
ticketsIds: [this.id],
withWarehouse: withWarehouse,
negative: true // Asumimos que queremos cantidades negativas para reembolsos
};
const query = 'Tickets/cloneAll';
return this.$http.post(query, params)
.then(res => {
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
}));
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()', () => {
it('should make a query and open the sms dialog', () => {
controller.$.sms = {open: () => {}};