#5914 - hotFix-transferInvoice #1893

Merged
alexm merged 17 commits from 5914-hotFix-transferInvoice into test 2024-01-04 07:13:01 +00:00
33 changed files with 399 additions and 207 deletions

View File

@ -0,0 +1,7 @@
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `cplusInvoiceTyoeFk`;
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `invoiceCorrectionType_Fk33`;
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `invoiceCorrection_ibfk_1`;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `siiTypeInvoiceOut_FK` FOREIGN KEY (`siiTypeInvoiceOutFk`) REFERENCES `vn`.`siiTypeInvoiceOut`(id) ON UPDATE CASCADE;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `invoiceCorrectionType_FK` FOREIGN KEY (`invoiceCorrectionTypeFk`) REFERENCES `vn`.`invoiceCorrectionType`(id) ON UPDATE CASCADE;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `cplusRectificationType_FK` FOREIGN KEY (`cplusRectificationTypeFk`) REFERENCES `vn`.`cplusRectificationType`(id) ON UPDATE CASCADE;

View File

@ -0,0 +1,33 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`getTaxBases`()
BEGIN
/**
alexm marked this conversation as resolved Outdated

Los saltos de línea entre los comentarios no son necesarios.

Los saltos de línea entre los comentarios no son necesarios.
* Calcula y devuelve en número de bases imponibles postivas y negativas
* Requiere la tabla temporal tmp.ticketToInvoice(id)
*
* returns tmp.taxBases
*/
CREATE OR REPLACE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM tmp.ticketToInvoice;
CALL ticket_getTax(NULL);
DROP TEMPORARY TABLE IF EXISTS tmp.taxBases;
CREATE TEMPORARY TABLE tmp.taxBases
ENGINE = MEMORY
SELECT
SUM(taxableBase > 0) as positive,
alexm marked this conversation as resolved Outdated

Yo lo veo más claro así

SUM(IF(taxableBase > 0, TRUE, FALSE))positive,
SUM(IF(taxableBase < 0, TRUE; FALSE))negative

Yo lo veo más claro así SUM(IF(taxableBase > 0, TRUE, FALSE))positive, SUM(IF(taxableBase < 0, TRUE; FALSE))negative

vale pero al sumar sí gastem 1 i 0

vale pero al sumar sí gastem 1 i 0
SUM(taxableBase < 0) as negative
FROM(
SELECT SUM(taxableBase) taxableBase
alexm marked this conversation as resolved Outdated

Se pueden omitir todos los 'as'

Se pueden omitir todos los 'as'
FROM tmp.ticketTax
GROUP BY pgcFk
) t;
END$$
DELIMITER ;

View File

@ -0,0 +1,30 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`hasAnyPositiveBase`() RETURNS tinyint(1)
DETERMINISTIC
BEGIN
/**
alexm marked this conversation as resolved Outdated

Los comentarios están mal, la forma correcta:

/**
*
*
*/

Los comentarios están mal, la forma correcta: /** * * */
* Calcula si existe alguna base imponible positiva
* Requiere la tabla temporal tmp.ticketToInvoice(id) para getTaxBases()
*
* returns BOOLEAN
*/
DECLARE hasAnyPositiveBase BOOLEAN;
CALL getTaxBases();
SELECT positive INTO hasAnyPositiveBase
alexm marked this conversation as resolved Outdated

sino poses un limit uno açò pot donar error si hi ha 2 en el into

sino poses un limit uno açò pot donar error si hi ha 2 en el into
FROM tmp.taxBases
LIMIT 1;
DROP TEMPORARY TABLE
tmp.ticketTax,
tmp.ticket,
tmp.taxBases;
RETURN hasAnyPositiveBase;
END$$
DELIMITER ;

View File

@ -0,0 +1,32 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`hasAnyNegativeBase`() RETURNS tinyint(1)
DETERMINISTIC
BEGIN
/**
* Calcula si existe alguna base imponible negativa
* Requiere la tabla temporal tmp.ticketToInvoice(id) para getTaxBases()
*
* returns BOOLEAN
*/
DECLARE hasAnyNegativeBase BOOLEAN;
CALL getTaxBases();
SELECT negative INTO hasAnyNegativeBase
alexm marked this conversation as resolved Outdated

limit 1

limit 1
FROM tmp.taxBases
LIMIT 1;
DROP TEMPORARY TABLE
tmp.ticketTax,
tmp.ticket,
tmp.taxBases;
RETURN hasAnyNegativeBase;
END$$
DELIMITER ;

View File

@ -606,6 +606,7 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
('T', 'Española rapida', 1, 'NATIONAL', 0, 'quick'),
('V', 'Intracomunitaria global', 0, 'CEE', 1, 'global'),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0, 'quick'),
('R', 'Rectificativa', 1, 'NATIONAL', 0, NULL),
Review

He afegit la rectificativa

He afegit la rectificativa
('E', 'Exportación rápida', 0, 'WORLD', 0, 'quick');
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
@ -613,7 +614,7 @@ INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `cr
(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),
(4, 'T', 8.88, util.VN_CURDATE(), 1104, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
Review

Li he donat coherencia a alguna fixture

Li he donat coherencia a alguna fixture
(5, 'A', 8.88, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 442, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 0);
UPDATE `vn`.`invoiceOut` SET ref = 'T1111111' WHERE id = 1;
@ -719,7 +720,7 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`)
VALUES
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 1, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2),
Review

Li he donat coherencia a alguna fixture

Li he donat coherencia a alguna fixture
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL),

View File

@ -35,7 +35,7 @@ describe('Ticket index payout path', () => {
await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton);
await page.write(selectors.ticketsIndex.advancedSearchClient, '1101');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 9);
await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 10);
await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox);

View File

@ -28,7 +28,6 @@ describe('InvoiceOut summary path', () => {
it('should contain the tax breakdown', async() => {
const firstTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxOne, 'innerText');
const secondTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxTwo, 'innerText');
expect(firstTax).toContain('10%');
@ -37,10 +36,9 @@ describe('InvoiceOut summary path', () => {
it('should contain the tickets info', async() => {
const firstTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketOne, 'innerText');
const secondTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketTwo, 'innerText');
expect(firstTicket).toContain('Bat cave');
expect(secondTicket).toContain('Stark tower');
expect(secondTicket).toContain('Bat cave');
});
});

View File

@ -45,6 +45,7 @@
"Extension format is invalid": "Extension format is invalid",
"NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS",
"This client can't be invoiced": "This client can't be invoiced",
"You must provide the correction information to generate a corrective invoice": "You must provide the correction information to generate a corrective invoice",
"The introduced hour already exists": "The introduced hour already exists",
"Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket",
"Concept cannot be blank": "Concept cannot be blank",
@ -178,7 +179,8 @@
"The renew period has not been exceeded": "The renew period has not been exceeded",
"You can not use the same password": "You can not use the same password",
"Valid priorities": "Valid priorities: %d",
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}",
"hasAnyNegativeBase": "Negative basis of tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Positive basis of tickets: {{ticketsIds}}",
"This ticket cannot be left empty.": "This ticket cannot be left empty. %s",
"Social name should be uppercase": "Social name should be uppercase",
"Street should be uppercase": "Street should be uppercase",

View File

@ -72,6 +72,7 @@
"The secret can't be blank": "La contraseña no puede estar en blanco",
"We weren't able to send this SMS": "No hemos podido enviar el SMS",
"This client can't be invoiced": "Este cliente no puede ser facturado",
"You must provide the correction information to generate a corrective invoice": "Debes informar la información de corrección para generar una factura rectificativa",
"This ticket can't be invoiced": "Este ticket no puede ser facturado",
"You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado",
"This ticket can not be modified": "Este ticket no puede ser modificado",
@ -305,7 +306,8 @@
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Valid priorities": "Prioridades válidas: %d",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}",
"hasAnyNegativeBase": "Base negativa para los tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Base positivas para los tickets: {{ticketsIds}}",
"You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado",
"This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s",
"The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias",
@ -329,5 +331,6 @@
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mínima",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas",
"No tickets to invoice": "No hay tickets para facturar",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}"
}

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', {
Self.remoteMethod('canBeInvoiced', {
description: 'Change property isEqualizated in all client addresses',
accessType: 'READ',
accepts: [

View File

@ -16,7 +16,7 @@ describe('client consumption() filter', () => {
};
const result = await models.Client.consumption(ctx, filter, options);
expect(result.length).toEqual(10);
expect(result.length).toEqual(11);
await tx.rollback();
} catch (e) {
@ -49,7 +49,7 @@ describe('client consumption() filter', () => {
const thirdRow = result[2];
expect(result.length).toEqual(3);
expect(firstRow.quantity).toEqual(10);
expect(firstRow.quantity).toEqual(11);
expect(secondRow.quantity).toEqual(15);
expect(thirdRow.quantity).toEqual(20);

View File

@ -80,6 +80,7 @@ module.exports = Self => {
invoiceType,
args.companyFk,
args.invoiceDate,
null,
options
);

View File

@ -2,7 +2,7 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('InvoiceOut tranferInvoice()', () => {
describe('InvoiceOut transferInvoice()', () => {
const activeCtx = {
accessToken: {userId: 5},
http: {
@ -23,20 +23,29 @@ describe('InvoiceOut tranferInvoice()', () => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
ref: 'T4444444',
id: '4',
refFk: 'T4444444',
newClientFk: 1,
cplusRectificationId: 1,
siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1
cplusRectificationTypeFk: 1,
siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeFk: 1
};
ctx.args = args;
try {
const {clientFk: oldClient} = await models.InvoiceOut.findById(args.id, {fields: ['clientFk']});
const invoicesBefore = await models.InvoiceOut.find({}, options);
const result = await models.InvoiceOut.transferInvoice(
ctx,
options);
const invoicesAfter = await models.InvoiceOut.find({}, options);
const rectificativeInvoice = invoicesAfter[invoicesAfter.length - 2];
const newInvoice = invoicesAfter[invoicesAfter.length - 1];
expect(result).toBeDefined();
expect(invoicesAfter.length - invoicesBefore.length).toEqual(2);
expect(rectificativeInvoice.clientFk).toEqual(oldClient);
expect(newInvoice.clientFk).toEqual(args.newClientFk);
await tx.rollback();
} catch (e) {
await tx.rollback();
@ -49,20 +58,44 @@ describe('InvoiceOut tranferInvoice()', () => {
const options = {transaction: tx};
const args = {
id: '1',
ref: 'T1111111',
refFk: 'T1111111',
newClientFk: 1101,
cplusRectificationId: 1,
siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1
cplusRectificationTypeFk: 1,
siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeFk: 1
};
ctx.args = args;
try {
await models.InvoiceOut.transferInvoice(
ctx,
options);
await tx.rollback();
} catch (e) {
expect(e.message).toBe(`Select a different client`);
await tx.rollback();
}
});
it('should throw an UserError when it is refund', async() => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
refFk: 'T1111111',
newClientFk: 1102,
cplusRectificationTypeFk: 1,
siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeFk: 1
};
ctx.args = args;
try {
await models.InvoiceOut.transferInvoice(
ctx,
options);
await tx.rollback();
} catch (e) {
expect(e.message).toContain(`This ticket is already a refund`);
await tx.rollback();
}
});
});

View File

@ -12,7 +12,7 @@ module.exports = Self => {
description: 'Issued invoice id'
},
{
arg: 'ref',
arg: 'refFk',
type: 'string',
required: true
},
@ -22,17 +22,17 @@ module.exports = Self => {
required: true
},
{
arg: 'cplusRectificationId',
arg: 'cplusRectificationTypeFk',
type: 'number',
required: true
},
{
arg: 'siiTypeInvoiceOutId',
arg: 'siiTypeInvoiceOutFk',
type: 'number',
required: true
},
{
arg: 'invoiceCorrectionTypeId',
arg: 'invoiceCorrectionTypeFk',
type: 'number',
required: true
},
@ -50,14 +50,14 @@ module.exports = Self => {
Self.transferInvoice = async(ctx, options) => {
const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId};
const args = ctx.args;
const {id, refFk, newClientFk, cplusRectificationTypeFk, siiTypeInvoiceOutFk, invoiceCorrectionTypeFk} = ctx.args;
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
const {clientFk} = await models.InvoiceOut.findById(args.id);
const {clientFk} = await models.InvoiceOut.findById(id);
if (clientFk == args.newClientFk)
if (clientFk == newClientFk)
throw new UserError(`Select a different client`);
if (!myOptions.transaction) {
@ -65,10 +65,10 @@ module.exports = Self => {
myOptions.transaction = tx;
}
try {
const filterRef = {where: {refFk: args.ref}};
const filterRef = {where: {refFk: refFk}};
const tickets = await models.Ticket.find(filterRef, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id);
await models.Ticket.refund(ctx, ticketsIds, null, myOptions);
const refundTickets = await models.Ticket.refund(ctx, ticketsIds, null, myOptions);
const filterTicket = {where: {ticketFk: {inq: ticketsIds}}};
@ -82,20 +82,16 @@ module.exports = Self => {
const clonedTicketIds = [];
for (const clonedTicket of clonedTickets) {
await clonedTicket.updateAttribute('clientFk', args.newClientFk, myOptions);
await clonedTicket.updateAttribute('clientFk', newClientFk, myOptions);
clonedTicketIds.push(clonedTicket.id);
}
const invoiceIds = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, myOptions);
const [invoiceId] = invoiceIds;
const invoiceCorrection =
{correctedFk: id, cplusRectificationTypeFk, siiTypeInvoiceOutFk, invoiceCorrectionTypeFk};
const refundTicketIds = refundTickets.map(ticket => ticket.id);
await models.InvoiceCorrection.create({
correctingFk: invoiceId,
correctedFk: args.id,
cplusRectificationTypeFk: args.cplusRectificationId,
siiTypeInvoiceOutFk: args.siiTypeInvoiceOutId,
invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId
}, myOptions);
await models.Ticket.invoiceTickets(ctx, refundTicketIds, invoiceCorrection, myOptions);
const [invoiceId] = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, null, myOptions);
if (tx) {
await tx.commit();

View File

@ -16,13 +16,43 @@
"type": "number"
},
"cplusRectificationTypeFk": {
"type": "number"
"type": "number",
"required": true
},
"siiTypeInvoiceOutFk": {
"type": "number"
"type": "number",
"required": true
},
"invoiceCorrectionTypeFk": {
"type": "number"
"type": "number",
"required": true
},
"relations": {
"correcting": {
alexm marked this conversation as resolved Outdated

No hay relación con las tablas de "types"?

No hay relación con las tablas de "types"?
"type": "belongsTo",
"model": "InvoiceOut",
"foreignKey": "correctingFk"
},
"corrected": {
"type": "belongsTo",
"model": "InvoiceOut",
"foreignKey": "correctedFk"
},
"cplusRectificationType": {
"type": "belongsTo",
"model": "cplusRectificationType",
"foreignKey": "cplusRectificationTypeFk"
},
"siiTypeInvoiceOut": {
"type": "belongsTo",
"model": "siiTypeInvoiceOut",
"foreignKey": "siiTypeInvoiceOutFk"
},
"invoiceCorrectionType": {
"type": "belongsTo",
"model": "invoiceCorrectionType",
"foreignKey": "invoiceCorrectionTypeFk"
}
}
}
}

View File

@ -12,6 +12,9 @@
"type": "number",
"description": "Identifier"
},
"code": {
"type": "string"
},
"description": {
"type": "string"
}

View File

@ -7,7 +7,8 @@
<vn-crud-model
auto-load="true"
url="SiiTypeInvoiceOuts"
data="siiTypeInvoiceOut">
data="siiTypeInvoiceOuts"
where="{code: {like: 'R%'}}">
</vn-crud-model>
<vn-crud-model
auto-load="true"
@ -185,6 +186,9 @@
vn-id="transferInvoice"
title="transferInvoice"
on-accept="$ctrl.transferInvoice()">
<tpl-title translate>
transferInvoice
</tpl-title>
<tpl-body>
<section class="transferInvoice">
<vn-horizontal>
@ -197,8 +201,7 @@
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"
ng-model="$ctrl.clientId"
order="id">
<tpl-item>
#{{id}} - {{::name}}
@ -213,7 +216,7 @@
value-field="id"
ng-model="$ctrl.cplusRectificationType"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Cplus Type">
label="Rectificative type">
<tpl-item>
{{::description}}
</tpl-item>
@ -222,14 +225,17 @@
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="cplusInvoiceType"
data="siiTypeInvoiceOut"
vn-id="siiTypeInvoiceOut"
data="siiTypeInvoiceOuts"
show-field="description"
value-field="id"
fields="['id','code','description']"
required="true"
ng-model="$ctrl.siiTypeInvoiceOut"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Class">
<tpl-item>
{{::code}} - {{::description}}
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
vn-one

View File

@ -129,15 +129,15 @@ class Controller extends Section {
transferInvoice() {
const params = {
id: this.invoiceOut.id,
ref: this.invoiceOut.ref,
newClientFk: this.invoiceOut.client.id,
cplusRectificationId: this.cplusRectificationType,
siiTypeInvoiceOutId: this.siiTypeInvoiceOut,
invoiceCorrectionTypeId: this.invoiceCorrectionType
refFk: this.invoiceOut.ref,
newClientFk: this.clientId,
cplusRectificationTypeFk: this.cplusRectificationType,
siiTypeInvoiceOutFk: this.siiTypeInvoiceOut,
invoiceCorrectionTypeFk: this.invoiceCorrectionType
};
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {
const invoiceId = res.data;
this.vnApp.showSuccess(this.$t('Invoice trasfered!'));
this.vnApp.showSuccess(this.$t('Transferred invoice'));
this.$state.go('invoiceOut.card.summary', {id: invoiceId});
});
}

View File

@ -22,4 +22,5 @@ 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
Rectificative type: Tipo rectificativa
Transferred invoice: Factura transferida
alexm marked this conversation as resolved Outdated

ahir diguerem de no posar signes, de fet en la resposta al client jo tampoc ho posaria

ahir diguerem de no posar signes, de fet en la resposta al client jo tampoc ho posaria

View File

@ -6,6 +6,7 @@
auto-load="true"
url="InvoiceOutSerials"
data="invoiceOutSerials"
where="{code: {neq: 'R'}}"
order="code">
</vn-crud-model>
<vn-crud-model

View File

@ -19,7 +19,7 @@ module.exports = Self => {
}
],
returns: {
type: ['number'],
type: ['object'],
root: true
},
http: {
@ -54,7 +54,7 @@ module.exports = Self => {
if (tx) await tx.commit();
return refundsTicket[0];
return refundsTicket;
} catch (e) {
if (tx) await tx.rollback();
throw e;

View File

@ -23,9 +23,9 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const refundedTicket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
const refundedTickets = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
expect(refundedTicket).toBeDefined();
expect(refundedTickets).toBeDefined();
await tx.rollback();
} catch (e) {
@ -42,11 +42,11 @@ describe('Sale refund()', () => {
const options = {transaction: tx};
const ticketsBefore = await models.Ticket.find({}, options);
const ticket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
const tickets = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({
where: {
id: ticket.id
id: tickets[0].id
},
include: [
{

View File

@ -10,20 +10,20 @@ module.exports = function(Self) {
description: 'The tickets id',
type: ['number'],
required: true
},
{
arg: 'isRectificative',
description: 'If it is rectificative',
type: 'boolean'
}
],
returns: {
arg: 'data',
type: 'boolean',
root: true
},
http: {
path: `/canBeInvoiced`,
verb: 'get'
}
});
Self.canBeInvoiced = async(ctx, ticketsIds, options) => {
Self.canBeInvoiced = async(ctx, ticketsIds, isRectificative, options) => {
const myOptions = {};
const $t = ctx.req.__; // $translate
@ -34,26 +34,14 @@ module.exports = function(Self) {
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'refFk', 'shipped', 'totalWithVat', 'companyFk']
fields: ['id', 'refFk', 'shipped', 'totalWithVat']
}, myOptions);
const [firstTicket] = tickets;
const companyFk = firstTicket.companyFk;
const query =
`SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await Self.rawSql(query, [companyFk], options);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
const [result] = await Self.rawSql('SELECT hasAnyNegativeBase() AS base', null, options);
const hasAnyNegativeBase = result?.base && isSpanishCompany;
if (hasAnyNegativeBase)
throw new UserError($t('Negative basis of tickets', {ticketsIds: ticketsIds}));
const taxBaseFunction = isRectificative ? 'hasAnyPositiveBase' : 'hasAnyNegativeBase';
const [hasAnyIncorrectBase] =
await Self.rawSql(`SELECT ${taxBaseFunction}() AS hasBasesProblem`, null, options);
if (hasAnyIncorrectBase?.hasBasesProblem)
throw new UserError($t(taxBaseFunction, {ticketsIds: ticketsIds}));
const today = Date.vnNew();
tickets.some(ticket => {
@ -70,7 +58,5 @@ module.exports = function(Self) {
if (ticketsIds.length == 1 && priceZero)
throw new UserError(`A ticket with an amount of zero can't be invoiced`);
});
return true;
};
};

View File

@ -10,7 +10,13 @@ module.exports = function(Self) {
description: 'The tickets id',
type: ['number'],
required: true
},
{
arg: 'invoiceCorrection',
description: 'The invoice correction',
type: 'object',
}
],
returns: {
type: ['object'],
@ -22,7 +28,7 @@ module.exports = function(Self) {
}
});
Self.invoiceTickets = async(ctx, ticketsIds, options) => {
Self.invoiceTickets = async(ctx, ticketsIds, invoiceCorrection, options) => {
const models = Self.app.models;
const date = Date.vnNew();
date.setHours(0, 0, 0, 0);
@ -41,10 +47,10 @@ module.exports = function(Self) {
let invoicesIds = [];
try {
const tickets = await models.Ticket.find({
fields: ['id', 'clientFk', 'companyFk', 'addressFk'],
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'clientFk', 'companyFk']
}
}, myOptions);
alexm marked this conversation as resolved
Review

Defines clientId y companyId que vienen de firstTicket, pero porque no haces lo mismo con addressFk?

Defines clientId y companyId que vienen de firstTicket, pero porque no haces lo mismo con addressFk?
Review

Como solo lo uso una vez no lo he hecho variable.
En el caso de si que se usaban mas de una vez clientId y companyId.

Pero con el refactor companyId solo se usa una vez, ahora quito la variable

Como solo lo uso una vez no lo he hecho variable. En el caso de si que se usaban mas de una vez clientId y companyId. Pero con el refactor companyId solo se usa una vez, ahora quito la variable
const [firstTicket] = tickets;
jsegarra marked this conversation as resolved
Review

Traes un Array, pero luego seleccionas el primero, porque no haces findOne y cambias el nombre de la variable de la línea 49?

Traes un Array<Tickets>, pero luego seleccionas el primero, porque no haces findOne y cambias el nombre de la variable de la línea 49?
@ -55,22 +61,20 @@ module.exports = function(Self) {
if (!isSameClient)
throw new UserError(`You can't invoice tickets from multiple clients`);
const client = await models.Client.findById(clientId, {
fields: ['id', 'hasToInvoiceByAddress']
const {hasToInvoiceByAddress} = await models.Client.findById(clientId, {
fields: ['hasToInvoiceByAddress']
alexm marked this conversation as resolved Outdated

La propiedad id no la usas, al igual que en la línea 50, aunque es poco significativo.

La propiedad id no la usas, al igual que en la línea 50, aunque es poco significativo.
}, myOptions);
if (client.hasToInvoiceByAddress) {
const query = `
SELECT DISTINCT addressFk
FROM ticket t
WHERE id IN (?)`;
const result = await Self.rawSql(query, [ticketsIds], myOptions);
let ticketsByAddress = hasToInvoiceByAddress
alexm marked this conversation as resolved Outdated

Asignas unos valores que luego puede que se machaquen
Quizás declararia la variable vacía y pondria un else, o también mover a un método aparte

Asignas unos valores que luego puede que se machaquen Quizás declararia la variable vacía y pondria un else, o también mover a un método aparte
? Object.values(tickets.reduce((group, {id, addressFk}) => {
alexm marked this conversation as resolved Outdated

Si es la única llamada a client, porque no haces const { hasToInvoiceByAddress} en la línea 64

Si es la única llamada a client, porque no haces const { hasToInvoiceByAddress} en la línea 64
group[addressFk] = group[addressFk] ?? [];
alexm marked this conversation as resolved
Review

Todo esto es para hacer un Set de los Ids de tickets?
Porque más abajo haces object.values entonces las keys(addressFk) te dan igual, correcto?

Todo esto es para hacer un Set de los Ids de tickets? Porque más abajo haces object.values entonces las keys(addressFk) te dan igual, correcto?
group[addressFk].push(id);
return group;
}, {}))
: [ticketsIds];
const addressIds = result.map(address => address.addressFk);
for (const address of addressIds)
await createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions);
} else
await createInvoice(ctx, companyId, ticketsIds, null, invoicesIds, myOptions);
for (const ticketIds of ticketsByAddress)
invoicesIds.push(await createInvoice(ctx, companyId, ticketIds, invoiceCorrection, myOptions));
if (tx) await tx.commit();
} catch (e) {
@ -85,9 +89,8 @@ module.exports = function(Self) {
return invoicesIds;
};
async function createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions) {
async function createInvoice(ctx, companyId, ticketsIds, invoiceCorrection, myOptions) {
const models = Self.app.models;
await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
@ -95,11 +98,8 @@ module.exports = function(Self) {
SELECT id
FROM vn.ticket
WHERE id IN (?)
${address ? `AND addressFk = ${address}` : ''}
`, [ticketsIds], myOptions);
const invoiceId = await models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), myOptions);
invoicesIds.push(invoiceId);
return models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), invoiceCorrection, myOptions);
}
};

View File

@ -22,6 +22,11 @@ module.exports = function(Self) {
description: 'The invoice date',
type: 'date',
required: true
},
{
arg: 'invoiceCorrection',
description: 'The invoice correction',
type: 'object',
}
],
returns: {
@ -34,7 +39,7 @@ module.exports = function(Self) {
}
});
Self.makeInvoice = async(ctx, invoiceType, companyFk, invoiceDate, options) => {
Self.makeInvoice = async(ctx, invoiceType, companyFk, invoiceDate, invoiceCorrection, options) => {
const models = Self.app.models;
invoiceDate.setHours(0, 0, 0, 0);
@ -62,20 +67,24 @@ module.exports = function(Self) {
fields: ['id', 'clientFk', 'addressFk']
}, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, !!invoiceCorrection, myOptions);
const [firstTicket] = tickets;
const clientId = firstTicket.clientFk;
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, companyFk, myOptions);
const clientCanBeInvoiced =
await models.Client.canBeInvoiced(clientId, companyFk, myOptions);
if (!clientCanBeInvoiced)
throw new UserError(`This client can't be invoiced`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
const [{serial}] = await Self.rawSql(query, [
const [{serial}] = invoiceCorrection ? [{serial: 'R'}] : await Self.rawSql(
`SELECT vn.invoiceSerial(?, ?, ?) AS serial`,
[
clientId,
companyFk,
invoiceType,
], myOptions);
invoiceType
],
myOptions);
const invoiceOutSerial = await models.InvoiceOutSerial.findById(serial);
if (invoiceOutSerial?.taxAreaFk == 'WORLD') {
@ -87,10 +96,16 @@ module.exports = function(Self) {
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);
if (!resultInvoice)
if (!resultInvoice?.id)
throw new UserError('No tickets to invoice', 'notInvoiced');
if (serial != 'R' && resultInvoice.id)
if (invoiceCorrection) {
await models.InvoiceCorrection.create(
Object.assign(invoiceCorrection, {correctingFk: resultInvoice.id}),
myOptions
);
}
await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions);
alexm marked this conversation as resolved Outdated

Puede ser resultInvoice.id nulo? Porque en la línea 99 ya se emitiría un error

Puede ser resultInvoice.id nulo? Porque en la línea 99 ya se emitiría un error
if (tx) await tx.commit();

View File

@ -15,7 +15,7 @@ module.exports = Self => {
}
],
returns: {
type: ['number'],
type: ['object'],
root: true
},
http: {

View File

@ -27,10 +27,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false);
await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
alexm marked this conversation as resolved
Review

no hi ha cap test a de canBeInvoiced amb el 3 parametro a true

no hi ha cap test a de canBeInvoiced amb el 3 parametro a true
await tx.rollback();
} catch (e) {
error = e;
@ -59,10 +56,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false);
await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
await tx.rollback();
} catch (e) {
error = e;
@ -95,10 +89,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(false);
await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
await tx.rollback();
} catch (e) {
error = e;
@ -123,14 +114,36 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?)
`, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options);
expect(canBeInvoiced).toEqual(true);
await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return falsy for a ticket has positiveBase', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
await models.Ticket.canBeInvoiced(ctx, [ticketId], true, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`hasAnyPositiveBase`);
});
});

View File

@ -31,7 +31,7 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx};
const ticketsIds = [11, 16];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback();
} catch (e) {
@ -57,7 +57,7 @@ describe('ticket invoiceTickets()', () => {
await client.updateAttribute('isTaxDataChecked', false, options);
const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback();
} catch (e) {
@ -80,8 +80,8 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx};
const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback();
} catch (e) {
@ -102,7 +102,7 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx};
const ticketsIds = [11];
const invoicesIds = await models.Ticket.invoiceTickets(ctx, ticketsIds, options);
const invoicesIds = await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
expect(invoicesIds.length).toBeGreaterThan(0);

View File

@ -42,7 +42,7 @@ describe('ticket makeInvoice()', () => {
WHERE id IN (?)
`, [ticketsIds], options);
const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, null, options);
expect(invoiceId).toBeDefined();
@ -70,7 +70,7 @@ describe('ticket makeInvoice()', () => {
WHERE id IN (?)
`, [ticketsId], options);
await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, null, options);
await tx.rollback();
} catch (e) {
error = e;

View File

@ -292,7 +292,7 @@ class Controller extends Section {
const query = 'Tickets/refund';
return this.$http.post(query, params)
.then(res => {
const refundTicket = res.data;
const [refundTicket] = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));

View File

@ -262,11 +262,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
const params = {
ticketsIds: [16]
};
$httpBackend.expectPOST('Tickets/refund', params).respond({id: 99});
const response = {id: 99};
$httpBackend.expectPOST('Tickets/refund', params).respond([response]);
controller.refund();
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: 99});
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', response);
});
});

View File

@ -526,7 +526,7 @@ class Controller extends Section {
const params = {salesIds: salesIds, withWarehouse: withWarehouse};
const query = 'Sales/refund';
this.$http.post(query, params).then(res => {
const refundTicket = res.data;
const [refundTicket] = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));

View File

@ -729,7 +729,7 @@ describe('Ticket', () => {
salesIds: [1, 4],
};
const refundTicket = {id: 99};
$httpBackend.expect('POST', 'Sales/refund', params).respond(200, refundTicket);
$httpBackend.expect('POST', 'Sales/refund', params).respond(200, [refundTicket]);
controller.createRefund();
$httpBackend.flush();