5914-transferInvoiceOut #1761

Merged
jorgep merged 55 commits from 5914-transferInvoiceOut into dev 2023-10-26 12:39:11 +00:00
30 changed files with 589 additions and 145 deletions

View File

@ -0,0 +1,6 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('CplusInvoiceType477', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative');

View File

@ -604,7 +604,7 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES
(1, 'T', 1014.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(1, 'T', 1026.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(2, 'T', 121.36, util.VN_CURDATE(), 1102, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(3, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(4, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
@ -2977,3 +2977,9 @@ INSERT INTO vn.XDiario (id, ASIEN, FECHA, SUBCTA, CONTRA, CONCEPTO, EURODEBE, EU
INSERT INTO `vn`.`mistakeType` (`id`, `description`)
VALUES
(1, 'Incorrect quantity');
jorgep marked this conversation as resolved Outdated
Outdated
Review

En ingles

En ingles
INSERT INTO `vn`.`invoiceCorrectionType` (`id`, `description`)
VALUES
(1, 'Error in VAT calculation'),
(2, 'Error in sales details'),
(3, 'Error in customer data');

View File

@ -212,7 +212,7 @@ describe('Ticket Edit sale path', () => {
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('17');
await page.accessToSearchResult('15');
await page.accessToSection('ticket.card.sale');
});
@ -316,7 +316,7 @@ describe('Ticket Edit sale path', () => {
});
it('should confirm the transfered quantity is the correct one', async() => {
const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText');
const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleQuantityCell, 'innerText');
expect(result).toContain('20');
});
@ -370,7 +370,7 @@ describe('Ticket Edit sale path', () => {
await page.waitToClick(selectors.ticketSales.moveToNewTicketButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain(`You can't create a ticket for a inactive client`);
expect(message.text).toContain(`You can't create a ticket for an inactive client`);
await page.closePopup();
});

View File

@ -23,7 +23,7 @@
"Agency cannot be blank": "Agency cannot be blank",
"The IBAN does not have the correct format": "The IBAN does not have the correct format",
"You can't make changes on the basic data of an confirmed order or with rows": "You can't make changes on the basic data of an confirmed order or with rows",
"You can't create a ticket for a inactive client": "You can't create a ticket for a inactive client",
"You can't create a ticket for an inactive client": "You can't create a ticket for an inactive client",
"Worker cannot be blank": "Worker cannot be blank",
"You must delete the claim id %d first": "You must delete the claim id %d first",
"You don't have enough privileges": "You don't have enough privileges",
@ -188,7 +188,14 @@
"The ticket doesn't exist.": "The ticket doesn't exist.",
"The sales do not exists": "The sales do not exists",
"Ticket without Route": "Ticket without route",
"Select a different client": "Select a different client",
"Fill all the fields": "Fill all the fields",
"Error while generating PDF": "Error while generating PDF",
"Can't invoice to future": "Can't invoice to future",
"This ticket is already invoiced": "This ticket is already invoiced",
"Negative basis of tickets: 23": "Negative basis of tickets: 23",
"Booking completed": "Booking complete",
"The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
"You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets"
}

View File

@ -5,10 +5,10 @@
"The default consignee can not be unchecked": "No se puede desmarcar el consignatario predeterminado",
"Unable to default a disabled consignee": "No se puede poner predeterminado un consignatario desactivado",
"Can't be blank": "No puede estar en blanco",
"Invalid TIN": "NIF/CIF invalido",
"Invalid TIN": "NIF/CIF inválido",
"TIN must be unique": "El NIF/CIF debe ser único",
"A client with that Web User name already exists": "Ya existe un cliente con ese Usuario Web",
"Is invalid": "Is invalid",
"Is invalid": "Es inválido",
"Quantity cannot be zero": "La cantidad no puede ser cero",
"Enter an integer different to zero": "Introduce un entero distinto de cero",
"Package cannot be blank": "El embalaje no puede estar en blanco",
@ -55,17 +55,17 @@
"You must delete the claim id %d first": "Antes debes borrar la reclamación %d",
"You don't have enough privileges": "No tienes suficientes permisos",
"Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF",
"You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos",
"INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no esta permitido el uso de la letra ñ",
"You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos básicos de una orden con artículos",
"INVALID_USER_NAME": "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no está permitido el uso de la letra ñ",
"You can't create a ticket for a frozen client": "No puedes crear un ticket para un cliente congelado",
"You can't create a ticket for a inactive client": "No puedes crear un ticket para un cliente inactivo",
"You can't create a ticket for an inactive client": "No puedes crear un ticket para un cliente inactivo",
"Tag value cannot be blank": "El valor del tag no puede quedar en blanco",
"ORDER_EMPTY": "Cesta vacía",
"You don't have enough privileges to do that": "No tienes permisos para cambiar esto",
"NO SE PUEDE DESACTIVAR EL CONSIGNAT": "NO SE PUEDE DESACTIVAR EL CONSIGNAT",
"Error. El NIF/CIF está repetido": "Error. El NIF/CIF está repetido",
"Street cannot be empty": "Dirección no puede estar en blanco",
"City cannot be empty": "Cuidad no puede estar en blanco",
"City cannot be empty": "Ciudad no puede estar en blanco",
"Code cannot be blank": "Código no puede estar en blanco",
"You cannot remove this department": "No puedes eliminar este departamento",
"The extension must be unique": "La extensión debe ser unica",
@ -102,8 +102,8 @@
"You can't delete a confirmed order": "No puedes borrar un pedido confirmado",
"The social name has an invalid format": "El nombre fiscal tiene un formato incorrecto",
"Invalid quantity": "Cantidad invalida",
"This postal code is not valid": "This postal code is not valid",
"is invalid": "is invalid",
"This postal code is not valid": "Este código postal no es válido",
"is invalid": "es inválido",
"The postcode doesn't exist. Please enter a correct one": "El código postal no existe. Por favor, introduce uno correcto",
"The department name can't be repeated": "El nombre del departamento no puede repetirse",
"This phone already exists": "Este teléfono ya existe",
@ -112,8 +112,8 @@
"You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada",
"You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero",
"You should specify a date": "Debes especificar una fecha",
"You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fín",
"Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín",
"You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fin",
"Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fin",
"You should mark at least one week day": "Debes marcar al menos un día de la semana",
"Swift / BIC can't be empty": "Swift / BIC no puede estar vacío",
"Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios",
@ -144,15 +144,15 @@
"Unable to clone this travel": "No ha sido posible clonar este travel",
"This thermograph id already exists": "La id del termógrafo ya existe",
"Choose a date range or days forward": "Selecciona un rango de fechas o días en adelante",
"ORDER_ALREADY_CONFIRMED": "ORDER_ALREADY_CONFIRMED",
"ORDER_ALREADY_CONFIRMED": "ORDEN YA CONFIRMADA",
"Invalid password": "Invalid password",
"Password does not meet requirements": "La contraseña no cumple los requisitos",
"Role already assigned": "Role already assigned",
"Invalid role name": "Invalid role name",
"Role name must be written in camelCase": "Role name must be written in camelCase",
"Email already exists": "Email already exists",
"User already exists": "User already exists",
"Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral",
"Role already assigned": "Rol ya asignado",
"Invalid role name": "Nombre de rol no válido",
"Role name must be written in camelCase": "El nombre del rol debe escribirse en camelCase",
"Email already exists": "El correo ya existe",
"User already exists": "El/La usuario/a ya existe",
"Absence change notification on the labour calendar": "Notificación de cambio de ausencia en el calendario laboral",
"Record of hours week": "Registro de horas semana {{week}} año {{year}} ",
"Created absence": "El empleado <strong>{{author}}</strong> ha añadido una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> para el día {{dated}}.",
"Deleted absence": "El empleado <strong>{{author}}</strong> ha eliminado una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> del día {{dated}}.",
@ -317,6 +317,9 @@
"The ticket doesn't exist.": "No existe el ticket.",
"Social name should be uppercase": "La razón social debe ir en mayúscula",
"Street should be uppercase": "La dirección fiscal debe ir en mayúscula",
"Ticket without Route": "Ticket sin ruta",
"Select a different client": "Seleccione un cliente distinto",
"Fill all the fields": "Rellene todos los campos",
jorgep marked this conversation as resolved Outdated

"NIF/CIF invalido" podría llevar una tilde: "NIF/CIF inválido".
"Cuidad no puede estar en blanco" tiene un pequeño error tipográfico, debe ser "Ciudad no puede estar en blanco".
"Is invalid" y "is invalid" no se han traducido al español.
"This postal code is not valid" también está en inglés.
"Role already assigned", "Invalid role name", etc. están en inglés y no se han traducido.
"No puedes cambiar los datos basicos de una orden con artículos" podría llevar tilde en "básicos".
"El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no esta permitido el uso de la letra ñ" podría llevar tilde en "está"
"La fecha de inicio debe ser menor que la fecha de fín" sin tilde en "fin".
"Debes especificar una fecha de inicio o de fín" sin tilde en "fin".
[chatgpt]

"NIF/CIF invalido" podría llevar una tilde: "NIF/CIF inválido". "Cuidad no puede estar en blanco" tiene un pequeño error tipográfico, debe ser "Ciudad no puede estar en blanco". "Is invalid" y "is invalid" no se han traducido al español. "This postal code is not valid" también está en inglés. "Role already assigned", "Invalid role name", etc. están en inglés y no se han traducido. "No puedes cambiar los datos basicos de una orden con artículos" podría llevar tilde en "básicos". "El nombre de usuario solo debe contener letras minúsculas o, a partir del segundo carácter, números o subguiones, no esta permitido el uso de la letra ñ" podría llevar tilde en "está" "La fecha de inicio debe ser menor que la fecha de fín" sin tilde en "fin". "Debes especificar una fecha de inicio o de fín" sin tilde en "fin". [chatgpt]
"The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada",

View File

@ -23,7 +23,7 @@ module.exports = Self => {
}
});
Self.makePdfAndNotify = async function(ctx, id, printerFk) {
Self.makePdfAndNotify = async function(ctx, id, printerFk, options) {
const models = Self.app.models;
options = typeof options == 'object'

View File

@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('InvoiceOut refund()', () => {
const userId = 5;
const ctx = {req: {accessToken: userId}};
const ctx = {req: {accessToken: userId}, args: {}};
const withWarehouse = true;
const activeCtx = {
accessToken: {userId: userId},

View File

@ -0,0 +1,68 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('InvoiceOut tranferInvoice()', () => {
const activeCtx = {
accessToken: {userId: 5},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should return the id of the created issued invoice', async() => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
ref: 'T4444444',
newClientFk: 1,
cplusRectificationId: 1,
cplusInvoiceType477Id: 1,
invoiceCorrectionTypeId: 1
};
ctx.args = args;
try {
const result = await models.InvoiceOut.transferInvoice(
ctx,
options);
expect(result).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an UserError when it is the same client', async() => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
ref: 'T1111111',
newClientFk: 1101,
cplusRectificationId: 1,
cplusInvoiceType477Id: 1,
invoiceCorrectionTypeId: 1
};
ctx.args = args;
try {
await models.InvoiceOut.transferInvoice(
ctx,
options);
} catch (e) {
expect(e.message).toBe(`Select a different client`);
await tx.rollback();
}
});
});

View File

@ -0,0 +1,111 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('transferInvoice', {
description: 'Transfer an issued invoice to another client',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'Issued invoice id'
},
{
arg: 'ref',
type: 'string',
required: true
},
{
arg: 'newClientFk',
type: 'number',
required: true
},
{
arg: 'cplusRectificationId',
type: 'number',
required: true
},
{
arg: 'cplusInvoiceType477Id',
type: 'number',
required: true
},
{
arg: 'invoiceCorrectionTypeId',
type: 'number',
required: true
},
],
returns: {
type: 'boolean',
root: true
},
http: {
path: '/transferInvoice',
verb: 'post'
}
});
Self.transferInvoice = async(ctx, options) => {
const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId};
const args = ctx.args;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
jorgep marked this conversation as resolved Outdated

mejor client

mejor client
const {clientFk} = await models.InvoiceOut.findById(args.id);
jorgep marked this conversation as resolved
Review

Per a ficar esta restriccio pots ficar
required: true en les validacions (dalt)

Per a ficar esta restriccio pots ficar `required: true` en les validacions (dalt)
if (clientFk == args.newClientFk)
throw new UserError(`Select a different client`);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const filterRef = {where: {refFk: args.ref}};
const tickets = await models.Ticket.find(filterRef, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id);
await models.Ticket.refund(ctx, ticketsIds, null, myOptions);
const filterTicket = {where: {ticketFk: {inq: ticketsIds}}};
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, false, myOptions);
const clonedTicketIds = [];
for (const clonedTicket of clonedTickets) {
await clonedTicket.updateAttribute('clientFk', args.newClientFk, myOptions);
clonedTicketIds.push(clonedTicket.id);
}
const invoiceIds = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, myOptions);
const [invoiceId] = invoiceIds;
await models.InvoiceCorrection.create({
correctingFk: invoiceId,
correctedFk: args.id,
cplusRectificationTypeFk: args.cplusRectificationId,
cplusInvoiceType477Fk: args.cplusInvoiceType477Id,
invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId
}, myOptions);
if (tx) {
await tx.commit();
await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null);
}
return invoiceId;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -31,5 +31,17 @@
},
"ZipConfig": {
"dataSource": "vn"
},
"CplusRectificationType": {
"dataSource": "vn"
},
"InvoiceCorrectionType": {
"dataSource": "vn"
},
"InvoiceCorrection": {
"dataSource": "vn"
},
"CplusInvoiceType477": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "CplusInvoiceType477",
"base": "VnModel",
"options": {
"mysql": {
"table": "cplusInvoiceType477"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"description": {
"type": "string"
}
}
}

View File

@ -0,0 +1,19 @@
{
"name": "CplusRectificationType",
"base": "VnModel",
"options": {
"mysql": {
"table": "cplusRectificationType"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"description": {
"type": "string"
}
}
}

View File

@ -0,0 +1,19 @@
{
"name": "InvoiceCorrectionType",
"base": "VnModel",
"options": {
"mysql": {
"table": "invoiceCorrectionType"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"description": {
"type": "string"
}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "InvoiceCorrection",
"base": "VnModel",
"options": {
"mysql": {
"table": "invoiceCorrection"
}
},
"properties": {
"correctingFk": {
"id": true,
"type": "number",
"description": "Identifier"
},
"correctedFk": {
"type": "number"
},
"cplusRectificationTypeFk": {
"type": "number"
},
"cplusInvoiceType477Fk": {
"type": "number"
},
"invoiceCorrectionTypeFk": {
"type": "number"
}
}
}

View File

@ -23,6 +23,7 @@ module.exports = Self => {
require('../methods/invoiceOut/getInvoiceDate')(Self);
require('../methods/invoiceOut/negativeBases')(Self);
require('../methods/invoiceOut/negativeBasesCsv')(Self);
require('../methods/invoiceOut/transferInvoice')(Self);
Self.filePath = async function(id, options) {
const fields = ['ref', 'issued'];

View File

@ -1,3 +1,19 @@
<vn-crud-model
auto-load="true"
url="CplusRectificationTypes"
data="cplusRectificationTypes"
order="description">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="CplusInvoiceType477s"
data="cplusInvoiceType477">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="InvoiceCorrectionTypes"
data="invoiceCorrectionTypes">
</vn-crud-model>
<vn-icon-button
icon="more_vert"
@ -5,6 +21,13 @@
</vn-icon-button>
<vn-menu vn-id="menu">
<vn-list>
<vn-item
vn-acl="administrative"
jorgep marked this conversation as resolved
Review

fijate que aquí sí lo has puesto, pero en los acl no. En próximas ocasiones que vayan "juntos"

fijate que aquí sí lo has puesto, pero en los acl no. En próximas ocasiones que vayan "juntos"
vn-acl-action="remove"
ng-click="transferInvoice.show()"
translate>
Transfer invoice to...
</vn-item>
<vn-item class="dropdown"
vn-click-stop="showInvoiceMenu.show($event, 'left')"
name="showInvoicePdf"
@ -157,3 +180,71 @@
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<vn-dialog
vn-id="transferInvoice"
title="transferInvoice"
on-accept="$ctrl.transferInvoice()">
<tpl-body>
<section class="transferInvoice">
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="client"
required="true"
url="Clients"
label="Client"
show-field="name"
value-field="id"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
ng-model="$ctrl.invoiceOut.client.id"
initial-data="$ctrl.invoiceOut.client.id"
order="id">
<tpl-item>
#{{id}} - {{::name}}
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
vn-one
vn-id="cplusRectificationType"
required="true"
data="cplusRectificationTypes"
show-field="description"
value-field="id"
ng-model="$ctrl.cplusRectificationType"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Cplus Type">
<tpl-item>
{{::description}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="cplusInvoiceType"
data="cplusInvoiceType477"
show-field="description"
value-field="id"
required="true"
ng-model="$ctrl.cplusInvoiceType477"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Class">
</vn-autocomplete>
<vn-autocomplete
vn-one
vn-id="invoiceCorrectionType"
data="invoiceCorrectionTypes"
ng-model="$ctrl.invoiceCorrectionType"
show-field="description"
value-field="id"
required="true"
label="Type">
</vn-autocomplete>
</vn-horizontal>
</section>
</tpl-body>
<tpl-buttons>
<button response="accept" translate>Transfer client</button>
</tpl-buttons>
</vn-dialog>

View File

@ -125,6 +125,22 @@ class Controller extends Section {
this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
}
transferInvoice() {
const params = {
id: this.invoiceOut.id,
ref: this.invoiceOut.ref,
newClientFk: this.invoiceOut.client.id,
cplusRectificationId: this.cplusRectificationType,
cplusInvoiceType477Id: this.cplusInvoiceType477,
invoiceCorrectionTypeId: this.invoiceCorrectionType
};
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {
const invoiceId = res.data;
this.vnApp.showSuccess(this.$t('Invoice trasfered!'));
this.$state.go('invoiceOut.card.summary', {id: invoiceId});
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];

View File

@ -1 +1,3 @@
The following refund tickets have been created: "The following refund tickets have been created: {{ticketIds}}"
Transfer invoice to...: Transfer invoice to...
Cplus Type: Cplus Type

View File

@ -21,3 +21,5 @@ The invoice PDF document has been regenerated: El documento PDF de la factura ha
The email can't be empty: El correo no puede estar vacío
The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}"
Refund...: Abono...
Transfer invoice to...: Transferir factura a...
Cplus Type: Cplus Tipo

View File

@ -21,4 +21,10 @@ vn-invoice-out-descriptor-menu {
font-size: 1.75rem;
}
}
}
}
@media screen and (min-width: 1000px) {
jorgep marked this conversation as resolved
Review

el 1000 no lo podemos quitar?

el 1000 no lo podemos quitar?
.transferInvoice {
min-width: $width-md;
jorgep marked this conversation as resolved Outdated

@alexm valorad si es estrictamente necesario y si lo es no poner un 1000px si no algo relativo

@alexm valorad si es estrictamente necesario y si lo es no poner un 1000px si no algo relativo
}
}

View File

@ -0,0 +1,122 @@
module.exports = Self => {
jorgep marked this conversation as resolved
Review

Si quieres usar refundTickets en varios sitios no seria mejor ponerlo fuera de las funciones? (No estoy seguro de que funcione)

Si quieres usar refundTickets en varios sitios no seria mejor ponerlo fuera de las funciones? (No estoy seguro de que funcione)
Review

Mirado juntos.

Mirado juntos.
Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, group, negative, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
const newTickets = [];
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const salesFilter = {
where: {id: {inq: salesIds}},
include: {
relation: 'components',
scope: {
fields: ['saleFk', 'componentFk', 'value']
}
}
};
const sales = await models.Sale.find(salesFilter, myOptions);
let ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))];
const mappedTickets = new Map();
if (group) ticketsIds = [ticketsIds[0]];
jorgep marked this conversation as resolved Outdated
Outdated
Review
if(group) ticketsIds = ticketsIds[0]

                for (let ticketId of ticketsIds) {
                    await createTicketRefund(
                        ticketId,
                        withWarehouse,
                        refundTickets,
                        mappedTickets,
                        now,
                        myOptions
                    );
                }

y te ahorres fer if y else

``` if(group) ticketsIds = ticketsIds[0] for (let ticketId of ticketsIds) { await createTicketRefund( ticketId, withWarehouse, refundTickets, mappedTickets, now, myOptions ); } ``` y te ahorres fer if y else
for (let ticketId of ticketsIds) {
const newTicket = await createTicket(
ctx,
ticketId,
withWarehouse,
negative,
myOptions
jorgep marked this conversation as resolved
Review

Aqui

Aqui
);
newTickets.push(newTicket);
mappedTickets.set(ticketId, newTicket.id);
}
for (const sale of sales) {
const newTicketId = mappedTickets.get(sale.ticketFk);
jorgep marked this conversation as resolved Outdated
Outdated
Review

El map de mappedTickets realment el pots omplir fer el bucle

El map de mappedTickets realment el pots omplir fer el bucle
const createdSale = await models.Sale.create({
ticketFk: newTicketId,
itemFk: sale.itemFk,
quantity: negative ? - sale.quantity : sale.quantity,
concept: sale.concept,
price: sale.price,
discount: sale.discount,
}, myOptions);
const components = sale.components();
for (const component of components)
component.saleFk = createdSale.id;
await models.SaleComponent.create(components, myOptions);
}
if (servicesIds && servicesIds.length) {
const servicesFilter = {
jorgep marked this conversation as resolved Outdated
Outdated
Review

if (servicesIds && servicesIds.length)

if (servicesIds && servicesIds.length)
where: {id: {inq: servicesIds}}
};
const services = await models.TicketService.find(servicesFilter, myOptions);
for (const service of services) {
const newTicketId = mappedTickets.get(service.ticketFk);
await models.TicketService.create({
description: service.description,
quantity: negative ? - service.quantity : service.quantity,
price: service.price,
taxClassFk: service.taxClassFk,
ticketFk: newTicketId,
ticketServiceTypeFk: service.ticketServiceTypeFk,
}, myOptions);
}
}
if (tx) await tx.commit();
return newTickets;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
jorgep marked this conversation as resolved Outdated
Outdated
Review

Pero no quedarem en que clone.js no debia tindre res relacionat en refund

Pero no quedarem en que clone.js no debia tindre res relacionat en refund
async function createTicket(
ctx,
ticketId,
withWarehouse,
negative,
myOptions
) {
const models = Self.app.models;
const now = Date.vnNew();
const ticket = await models.Ticket.findById(ticketId, null, myOptions);
ctx.args.clientId = ticket.clientFk;
ctx.args.shipped = now;
ctx.args.landed = now;
ctx.args.warehouseId = withWarehouse ? ticket.warehouseFk : null;
ctx.args.companyId = ticket.companyFk;
ctx.args.addressId = ticket.addressFk;
const newTicket = await models.Ticket.new(ctx, myOptions);
if (negative) {
Review

En un futur se podria refactoritzar i extraure esta part a refund o un hipotetic ticket/clone

En un futur se podria refactoritzar i extraure esta part a refund o un hipotetic ticket/clone
await models.TicketRefund.create({
originalTicketFk: ticketId,
refundTicketFk: newTicket.id
}, myOptions);
}
jorgep marked this conversation as resolved Outdated
Outdated
Review

Esta funcio te la pots ahorrar no?
Fent mappedTickets.get(ticketId) ja te torna lo que vols pq quan has fet el set esta ja agrupat o no

Esta funcio te la pots ahorrar no? Fent mappedTickets.get(ticketId) ja te torna lo que vols pq quan has fet el set esta ja agrupat o no
return newTicket;
}
};
};

View File

@ -5,7 +5,8 @@ module.exports = Self => {
accepts: [
{
arg: 'salesIds',
type: ['number']
type: ['number'],
required: true
},
{
arg: 'servicesIds',
@ -40,122 +41,23 @@ module.exports = Self => {
myOptions.transaction = tx;
}
let refundTicket = null;
try {
const refundAgencyMode = await models.AgencyMode.findOne({
jorgep marked this conversation as resolved Outdated

esta logica donde esta ahora?

esta logica donde esta ahora?

Está dentro del método clone

Está dentro del método clone
include: {
relation: 'zones',
scope: {
limit: 1,
field: ['id', 'name']
}
},
where: {code: 'refund'}
}, myOptions);
const refoundZoneId = refundAgencyMode.zones()[0].id;
if (salesIds.length) {
const salesFilter = {
where: {id: {inq: salesIds}},
include: {
relation: 'components',
scope: {
fields: ['saleFk', 'componentFk', 'value']
}
}
};
const sales = await models.Sale.find(salesFilter, myOptions);
const ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))];
const now = Date.vnNew();
const [firstTicketId] = ticketsIds;
// eslint-disable-next-line max-len
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
for (const sale of sales) {
const createdSale = await models.Sale.create({
ticketFk: refundTicket.id,
itemFk: sale.itemFk,
quantity: - sale.quantity,
concept: sale.concept,
price: sale.price,
discount: sale.discount,
}, myOptions);
const components = sale.components();
for (const component of components)
component.saleFk = createdSale.id;
await models.SaleComponent.create(components, myOptions);
}
}
if (!refundTicket) {
const servicesFilter = {
where: {id: {inq: servicesIds}}
};
const services = await models.TicketService.find(servicesFilter, myOptions);
const firstTicketId = services[0].ticketFk;
const now = Date.vnNew();
// eslint-disable-next-line max-len
refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
}
if (servicesIds && servicesIds.length > 0) {
const servicesFilter = {
where: {id: {inq: servicesIds}}
};
const services = await models.TicketService.find(servicesFilter, myOptions);
for (const service of services) {
await models.TicketService.create({
description: service.description,
quantity: - service.quantity,
price: service.price,
taxClassFk: service.taxClassFk,
ticketFk: refundTicket.id,
ticketServiceTypeFk: service.ticketServiceTypeFk,
}, myOptions);
}
}
const query = `CALL vn.ticket_recalc(?, NULL)`;
await Self.rawSql(query, [refundTicket.id], myOptions);
const refundsTicket = await models.Sale.clone(
ctx,
salesIds,
servicesIds,
withWarehouse,
false,
true,
myOptions
);
jorgep marked this conversation as resolved
Review

Deuria ser per aci on fa que el ticket siga refund

Deuria ser per aci on fa que el ticket siga refund
if (tx) await tx.commit();
return refundTicket;
return refundsTicket[0];
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions) {
const models = Self.app.models;
const filter = {include: {relation: 'address'}};
const ticket = await models.Ticket.findById(ticketId, filter, myOptions);
const refundTicket = await models.Ticket.create({
clientFk: ticket.clientFk,
shipped: now,
addressFk: ticket.address().id,
agencyModeFk: refundAgencyMode.id,
nickname: ticket.address().nickname,
warehouseFk: withWarehouse ? ticket.warehouseFk : null,
companyFk: ticket.companyFk,
landed: now,
zoneFk: refoundZoneId
}, myOptions);
await models.TicketRefund.create({
refundTicketFk: refundTicket.id,
originalTicketFk: ticket.id,
}, myOptions);
return refundTicket;
}
};

View File

@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('Sale refund()', () => {
const userId = 5;
const ctx = {req: {accessToken: userId}};
const ctx = {req: {accessToken: userId}, args: {}};
const activeCtx = {
accessToken: {userId},
};
@ -40,6 +40,7 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const ticketsBefore = await models.Ticket.find({}, options);
const ticket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
@ -61,12 +62,13 @@ describe('Sale refund()', () => {
}
]
}, options);
const ticketsAfter = await models.Ticket.find({}, options);
const salesLength = refundedTicket.ticketSales().length;
const componentsLength = refundedTicket.ticketSales()[0].components().length;
expect(refundedTicket).toBeDefined();
expect(salesLength).toEqual(2);
expect(salesLength).toEqual(1);
expect(ticketsBefore.length).toEqual(ticketsAfter.length - 2);
expect(componentsLength).toEqual(4);
await tx.rollback();

View File

@ -77,9 +77,10 @@ module.exports = function(Self) {
if (tx) await tx.rollback();
throw e;
}
for (const invoiceId of invoicesIds)
await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null);
if (tx) {
for (const invoiceId of invoicesIds)
await models.InvoiceOut.makePdfAndNotify(ctx, invoiceId, null);
}
return invoicesIds;
};

View File

@ -96,7 +96,7 @@ module.exports = Self => {
if (address.client().type().code === 'normal' && (!agencyMode || agencyMode.code != 'refund')) {
const canCreateTicket = await models.Client.canCreateTicket(args.clientId, myOptions);
if (!canCreateTicket)
throw new UserError(`You can't create a ticket for a inactive client`);
throw new UserError(`You can't create a ticket for an inactive client`);
}
if (!args.shipped && args.landed) {

View File

@ -39,7 +39,6 @@ module.exports = Self => {
jorgep marked this conversation as resolved Outdated

modules/ticket/back/methods/ticket/invoiceTicketsWithPdf.js este porque sale?

modules/ticket/back/methods/ticket/invoiceTicketsWithPdf.js este porque sale?

Se me olvidó borrarlo

Se me olvidó borrarlo
try {
const filter = {where: {ticketFk: {inq: ticketsIds}}};
const sales = await models.Sale.find(filter, myOptions);
const salesIds = sales.map(sale => sale.id);

View File

@ -30,7 +30,7 @@ describe('ticket new()', () => {
await tx.rollback();
}
expect(error).toEqual(new UserError(`You can't create a ticket for a inactive client`));
expect(error).toEqual(new UserError(`You can't create a ticket for an inactive client`));
});
it('should throw an error if the address doesnt exist', async() => {

View File

@ -66,7 +66,7 @@ module.exports = Self => {
const ticket = await models.Ticket.findById(id);
const canCreateTicket = await models.Client.canCreateTicket(ticket.clientFk);
if (!canCreateTicket)
throw new UserError(`You can't create a ticket for a inactive client`);
throw new UserError(`You can't create a ticket for an inactive client`);
ticketId = await cloneTicket(originalTicket, myOptions);
}

View File

@ -12,6 +12,7 @@ module.exports = Self => {
require('../methods/sale/refund')(Self);
require('../methods/sale/canEdit')(Self);
require('../methods/sale/usesMana')(Self);
require('../methods/sale/clone')(Self);
Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank`

View File

@ -7,6 +7,7 @@ module.exports = {
mixins: [vnReport],
async serverPrefetch() {
this.invoice = await this.findOneFromDef('invoice', [this.reference]);
this.checkMainEntity(this.invoice);
this.client = await this.findOneFromDef('client', [this.reference]);
this.taxes = await this.rawSqlFromDef(`taxes`, [this.reference]);