Merge branch 'dev' into 3258-import_buys
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Joan Sanchez 2022-01-28 13:14:14 +00:00
commit 78507240ee
30 changed files with 2571 additions and 67 deletions

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES('InvoiceInDueDay', '*', '*', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1,248 @@
DROP PROCEDURE IF EXISTS vn.invoiceInBookingMain;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`invoiceInBookingMain`(vInvoiceInId INT)
BEGIN
DECLARE vTotalAmount,vTotalAmountDivisa DECIMAL(10,2);
DECLARE vBookNumber,vSerialNumber INT;
DECLARE vRate DECIMAL(10,4);
CALL invoiceInBookingCommon(vInvoiceInId,vSerialNumber);
SELECT SUM(iit.taxableBase * IF( i.serial= 'R' AND ti.Iva <> 'HP DEVENGADO 21 ISP', 1 +(ti.PorcentajeIva/100),1)),
SUM(iit.foreignValue * IF( i.serial= 'R', 1 + (ti.PorcentajeIva/100),1)),
iit.taxableBase/iit.foreignValue
INTO vTotalAmount, vTotalAmountDivisa, vRate
FROM newInvoiceIn i
JOIN invoiceInTax iit ON iit.invoiceInFk = i.id
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk;
CALL vn.ledger_next(vBookNumber);
-- Apunte del proveedor
INSERT INTO XDiario(ASIEN,
FECHA,
SUBCTA,
EUROHABER,
CONCEPTO,
CAMBIO,
HABERME,
NFACTICK,
CLAVE,
empresa_id
)
SELECT
vBookNumber,
n.bookEntried,
s.supplierAccount,
vTotalAmount EUROHABER,
n.conceptWithSupplier,
vRate,
vTotalAmountDivisa,
n.invoicesCount,
vInvoiceInId,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s;
-- Línea de Gastos
INSERT INTO XDiario ( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EURODEBE,
EUROHABER,
CONCEPTO,
CAMBIO,
DEBEME,
HABERME,
NFACTICK,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(e.isWithheld , LPAD(RIGHT(s.supplierAccount,5),10,iit.expenceFk),iit.expenceFk) SUBCTA,
s.supplierAccount CONTRA,
IF(e.isWithheld AND iit.taxableBase < 0, NULL, ROUND(SUM(iit.taxableBase),2)) EURODEBE,
IF(e.isWithheld AND iit.taxableBase < 0,ROUND(SUM(-iit.taxableBase),2),NULL) EUROHABER,
n.conceptWithSupplier CONCEPTO,
vRate,
IF(e.isWithheld,NULL,ABS(ROUND(SUM(iit.foreignValue),2))) DEBEME,
IF(e.isWithheld,ABS(ROUND(SUM(iit.foreignValue),2)),NULL) HABERME,
n.invoicesCount NFACTICK,
n.companyFk empresa_id
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax iit ON iit.invoiceInFk = n.id
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = iit.expenceFk
WHERE e.name != 'Suplidos Transitarios nacionales'
GROUP BY iit.expenceFk;
-- Líneas de IVA
INSERT INTO XDiario( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EURODEBE,
BASEEURO,
CONCEPTO,
FACTURA,
IVA,
AUXILIAR,
SERIE,
TIPOOPE,
FECHA_EX,
FECHA_OP,
NFACTICK,
FACTURAEX,
L340,
LRECT349,
TIPOCLAVE,
TIPOEXENCI,
TIPONOSUJE,
TIPOFACT,
TIPORECTIF,
TERIDNIF,
TERNIF,
TERNOM,
FECREGCON,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(n.expenceFkDeductible>0, n.expenceFkDeductible, ti.CuentaIvaSoportado) SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100 /* + 0.0001*/ , 2)) EURODEBE,
SUM(it.taxableBase) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
IF(isUeeMember AND eWithheld.id IS NULL,'','*') AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
TRUE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
n.cplusTrascendency472Fk TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.booked FECREGCON,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
LEFT JOIN (
SELECT eWithheld.id
FROM invoiceInTax hold
JOIN expence eWithheld ON eWithheld.id = hold.expenceFk AND eWithheld.isWithheld
WHERE hold.invoiceInFk = vInvoiceInId LIMIT 1
) eWithheld ON TRUE
WHERE it.taxTypeSageFk IS NOT NULL
AND it.taxTypeSageFk NOT IN (22, 90)
GROUP BY ti.PorcentajeIva, e.id;
-- Línea iva inversor sujeto pasivo
INSERT INTO XDiario( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EUROHABER,
BASEEURO,
CONCEPTO,
FACTURA,
IVA,
AUXILIAR,
SERIE,
TIPOOPE,
FECHA_EX,
FECHA_OP,
NFACTICK,
FACTURAEX,
L340,
LRECT349,
TIPOCLAVE,
TIPOEXENCI,
TIPONOSUJE,
TIPOFACT,
TIPORECTIF,
TERIDNIF,
TERNIF,
TERNOM,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
ti.CuentaIvaRepercutido SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100,2)) EUROHABER,
ROUND(SUM(it.taxableBase),2) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
'*' AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
FALSE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
1 TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
WHERE ti.Iva = 'HP DEVENGADO 21 ISP' OR MID(s.account, 4, 1) = '1'
GROUP BY ti.PorcentajeIva, e.id;
-- Actualización del registro original
UPDATE invoiceIn ii
JOIN newInvoiceIn ni ON ii.id = ni.id
SET ii.serialNumber = vSerialNumber,
ii.isBooked = TRUE;
-- Problemas derivados de la precisión en los decimales al calcular los impuestos
UPDATE XDiario
SET EURODEBE = EURODEBE -
(SELECT IF(ABS(sub.difference) = 0.01, sub.difference, 0)
FROM(
SELECT SUM(IFNULL(ROUND(EURODEBE, 2),0)) - SUM(IFNULL(ROUND(EUROHABER, 2), 0)) difference
FROM XDiario
WHERE ASIEN = vBookNumber
)sub
)
WHERE ASIEN = vBookNumber
AND EURODEBE <> 0
ORDER BY id DESC
LIMIT 1;
END$$
DELIMITER ;

View File

@ -137,7 +137,8 @@ module.exports = class Docker {
user: this.dbConf.username,
password: this.dbConf.password,
host: this.dbConf.host,
port: this.dbConf.port
port: this.dbConf.port,
connectTimeout: maxInterval
};
log('Waiting for MySQL init process...');

View File

@ -2353,7 +2353,7 @@ REPLACE INTO `vn`.`invoiceIn`(`id`, `serialNumber`,`serial`, `supplierFk`, `issu
INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`)
VALUES
(1, CURDATE(), 1, 237),
(1, CURDATE(), 1, 336.99),
(1, CURDATE(), 1, 15.25),
(2, CURDATE(), 1, 168),
(2, CURDATE(), 1, 55.17),

View File

@ -206,6 +206,12 @@ describe('Ticket Edit sale path', () => {
expect(message.text).toContain('Data saved!');
});
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
});
it('should select the third sale and create a pay back', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);

133
front/package-lock.json generated Normal file
View File

@ -0,0 +1,133 @@
{
"name": "salix-front",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@uirouter/angularjs": {
"version": "1.0.29",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.29.tgz",
"integrity": "sha512-RImWnBarNixkMto0o8stEaGwZmvhv5cnuOLXyMU2pY8MP2rgEF74ZNJTLeJCW14LR7XDUxVH8Mk8bPI6lxedmQ==",
"requires": {
"@uirouter/core": "6.0.7"
}
},
"@uirouter/core": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz",
"integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w=="
},
"angular": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.2.tgz",
"integrity": "sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw=="
},
"angular-animate": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
},
"angular-moment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": {
"moment": ">=2.8.0 <3.0.0"
}
},
"angular-translate": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
"integrity": "sha512-KohNrkH6J9PK+VW0L/nsRTcg5Fw70Ajwwe3Jbfm54Pf9u9Fd+wuingoKv+h45mKf38eT+Ouu51FPua8VmZNoCw==",
"requires": {
"angular": "^1.8.0"
}
},
"angular-translate-loader-partial": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.18.4.tgz",
"integrity": "sha512-bsjR+FbB0sdA2528E/ugwKdlPPQhA1looxLxI3otayBTFXBpED33besfSZhYAISLgNMSL038vSssfRUen9qD8w==",
"requires": {
"angular-translate": "~2.18.4"
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"croppie": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"mg-crud": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha1-p6AWGzWSPK7/8ZpIBpS2V1vDggw=",
"requires": {
"angular": "^1.6.1"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"oclazyload": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha1-Kjirv/QJDAihEBZxkZRbWfLoJ5w="
},
"require-yaml": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha1-LhsY2RPDuqcqWk03O28Tjd0sMr0=",
"requires": {
"js-yaml": "^4.1.0"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
}
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"validator": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g="
}
}
}

View File

@ -210,11 +210,13 @@
"Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio",
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente",
"Amounts do not match": "Las cantidades no coinciden",
"The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos",
"You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días",
"The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día",
"The worker has a marked absence that day": "El trabajador tiene marcada una ausencia ese día",
"You can not modify is pay method checked": "No se puede modificar el campo método de pago validado",
"Can't transfer claimed sales": "No puedes transferir lineas reclamadas"
}
"Can't transfer claimed sales": "No puedes transferir lineas reclamadas",
"You don't have privileges to create pay back": "No tienes permisos para crear un abono"
}

View File

@ -20,6 +20,9 @@
"MailForward": {
"dataSource": "vn"
},
"RoleConfig": {
"dataSource": "vn"
},
"RoleInherit": {
"dataSource": "vn"
},

View File

@ -114,17 +114,22 @@ module.exports = Self => {
'bcryptPassword',
'updated'
],
include: {
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
include: [
{
relation: 'roles',
scope: {
include: {
relation: 'inherits',
scope: {
fields: ['name']
}
}
}
}, {
relation: 'role',
fields: ['name']
}
}
]
});
let info = {

View File

@ -0,0 +1,103 @@
module.exports = Self => {
Self.getSynchronizer = async function() {
return await Self.findOne({fields: ['id']});
};
Object.assign(Self.prototype, {
async init() {
const [row] = await Self.rawSql('SELECT VERSION() AS `version`');
if (row.version.includes('MariaDB'))
this.dbType = 'MariaDB';
else
this.dbType = 'MySQL';
},
async syncUser(userName, info, password) {
const mysqlHost = '%';
let mysqlUser = userName;
if (this.dbType == 'MySQL') mysqlUser = `!${mysqlUser}`;
const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows
FROM mysql.user
WHERE User = ?
AND Host = ?`,
[mysqlUser, mysqlHost]
);
let userExists = row.nRows > 0;
let isUpdatable = true;
if (this.dbType == 'MariaDB') {
const [row] = await Self.rawSql(
`SELECT Priv AS priv
FROM mysql.global_priv
WHERE User = ?
AND Host = ?`,
[mysqlUser, mysqlHost]
);
const priv = row && JSON.parse(row.priv);
const role = priv && priv.default_role;
isUpdatable = !row || (role && role.startsWith('z-'));
}
if (!isUpdatable) {
console.warn(`RoleConfig.syncUser(): User '${userName}' cannot be updated, not managed by me`);
return;
}
if (info.hasAccount) {
if (password) {
if (!userExists) {
await Self.rawSql('CREATE USER ?@? IDENTIFIED BY ?',
[mysqlUser, mysqlHost, password]
);
userExists = true;
} else {
switch (this.dbType) {
case 'MariaDB':
await Self.rawSql('ALTER USER ?@? IDENTIFIED BY ?',
[mysqlUser, mysqlHost, password]
);
break;
default:
await Self.rawSql('SET PASSWORD FOR ?@? = PASSWORD(?)',
[mysqlUser, mysqlHost, password]
);
}
}
}
if (userExists && this.dbType == 'MariaDB') {
let role = `z-${info.user.role().name}`;
try {
await Self.rawSql('REVOKE ALL, GRANT OPTION FROM ?@?',
[mysqlUser, mysqlHost]
);
} catch (err) {
if (err.code == 'ER_REVOKE_GRANTS')
console.warn(`${err.code}: ${err.sqlMessage}: ${err.sql}`);
else
throw err;
}
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, mysqlHost]
);
if (role) {
await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?',
[role, mysqlUser, mysqlHost]
);
} else {
await Self.rawSql('SET DEFAULT ROLE NONE FOR ?@?',
[mysqlUser, mysqlHost]
);
}
}
} else if (userExists)
await Self.rawSql('DROP USER ?@?', [mysqlUser, mysqlHost]);
}
});
};

View File

@ -0,0 +1,21 @@
{
"name": "RoleConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.roleConfig"
}
},
"mixins": {
"AccountSynchronizer": {}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"mysqlPassword": {
"type": "string"
}
}
}

View File

@ -0,0 +1,43 @@
module.exports = Self => {
Self.remoteMethod('getTotals', {
description: 'Return totals for an invoiceIn',
accessType: 'READ',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'invoiceIn id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/getTotals',
verb: 'GET'
}
});
Self.getTotals = async(id, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const [result] = await Self.rawSql(`
SELECT iit.*,
SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat
FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
WHERE
ii.id = ?`, [id, id]);
return result;
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn getTotals()', () => {
it('should check that returns invoiceIn totals', async() => {
const invoiceInId = 1;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
const invoiceIntotals = await models.InvoiceIn.getTotals(invoiceInId, options);
expect(typeof invoiceIntotals.totalTaxableBase).toBe('number');
expect(invoiceIntotals.totalTaxableBase).toEqual(invoiceIntotals.totalDueDay);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,34 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn toBook()', () => {
it('should check that invoiceIn is booked', async() => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const invoiceInId = 1;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
const invoiceInBefore = await models.InvoiceIn.findById(invoiceInId, null, options);
expect(invoiceInBefore.isBooked).toEqual(false);
await models.InvoiceIn.toBook(ctx, invoiceInId, options);
const invoiceIn = await models.InvoiceIn.findById(invoiceInId, null, options);
expect(invoiceIn.isBooked).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -20,6 +20,7 @@ module.exports = Self => {
});
Self.summary = async(id, options) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
@ -85,25 +86,9 @@ module.exports = Self => {
}
]
};
let summaryObj = await models.InvoiceIn.findById(id, filter, myOptions);
let summaryObj = await Self.app.models.InvoiceIn.findById(id, filter, myOptions);
summaryObj.totals = await getTotals(id);
summaryObj.totals = await models.InvoiceIn.getTotals(id, myOptions);
return summaryObj;
};
async function getTotals(invoiceInFk) {
return (await Self.rawSql(`
SELECT iit.*,
SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat
FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
WHERE
ii.id = ?`, [invoiceInFk, invoiceInFk]))[0];
}
};

View File

@ -0,0 +1,42 @@
module.exports = Self => {
Self.remoteMethodCtx('toBook', {
description: 'To book the invoiceIn',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/toBook',
verb: 'POST'
}
});
Self.toBook = async(ctx, id, options) => {
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
await Self.rawSql(`CALL vn.invoiceInBookingMain(?)`, [id], myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -2,4 +2,6 @@ module.exports = Self => {
require('../methods/invoice-in/filter')(Self);
require('../methods/invoice-in/summary')(Self);
require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self);
};

View File

@ -1,8 +1,16 @@
<vn-descriptor-content module="invoiceIn" description="$ctrl.invoiceIn.supplierRef">
<slot-menu>
<vn-item
ng-click="$ctrl.checkToBook()"
vn-acl="administrative"
ng-hide="$ctrl.invoiceIn.isBooked == true"
translate>
To book
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="invoicing"
vn-acl="administrative"
vn-acl-action="remove"
name="deleteInvoice"
translate>
@ -10,7 +18,7 @@
</vn-item>
<vn-item
ng-click="cloneConfirmation.show()"
vn-acl="invoicing"
vn-acl="administrative"
name="cloneInvoice"
translate>
Clone Invoice
@ -46,7 +54,9 @@
icon="icon-invoice-in">
</vn-quick-link>
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm
@ -62,3 +72,8 @@
<vn-supplier-descriptor-popover
vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover>
<vn-confirm
vn-id="confirm-toBookAnyway"
message="Are you sure you want to book this invoice?"
on-accept="$ctrl.onAcceptToBook()">
</vn-confirm>

View File

@ -51,6 +51,43 @@ class Controller extends Descriptor {
return this.getData(`InvoiceIns/${this.id}`, {filter})
.then(res => this.entity = res.data);
}
checkToBook() {
let message = '';
const id = this.invoiceIn.id;
this.$q.all([
this.$http.get(`InvoiceIns/${this.id}/getTotals`)
.then(res => {
const taxableBaseNotEqualDueDay = res.data.totalDueDay != res.data.totalTaxableBase;
const vatNotEqualDueDay = res.data.totalDueDay != res.data.totalVat;
if (taxableBaseNotEqualDueDay && vatNotEqualDueDay)
message += 'amountsDoNotMatch';
}),
this.$http.get('InvoiceInDueDays/count', {
filter: {
where: {
invoiceInFk: id,
dueDated: {gte: new Date()}
}
}})
.then(res => {
if (res.data)
message += 'future payments';
})
]).finally(() => {
if (message.length)
this.$.confirmToBookAnyway.show();
else
this.onAcceptToBook();
});
}
onAcceptToBook() {
this.$http.post(`InvoiceIns/${this.id}/toBook`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
}
}
ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -8,19 +8,70 @@ describe('vnInvoiceInDescriptor', () => {
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnInvoiceInDescriptor', {$element: null});
const $element = angular.element('<vn-invoice-in-descriptor></vn-invoice-in-descriptor>');
controller = $componentController('vnInvoiceInDescriptor', {$element});
controller.invoiceIn = {id: 1};
$httpBackend.when('GET', `InvoiceIns/${controller.invoiceIn.id}`).respond({id: 1});
}));
describe('loadData()', () => {
it(`should perform a get query to store the invoice in data into the controller`, () => {
const id = 1;
const response = {id: 1};
expect(controller.invoiceIn).toEqual({id: 1});
});
});
$httpBackend.expectGET(`InvoiceIns/${id}`).respond(response);
controller.id = id;
describe('onAcceptToBook()', () => {
it(`should perform a post query to book the invoice`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.reload = jest.fn();
const id = 1;
$httpBackend.expectPOST(`InvoiceIns/${id}/toBook`).respond();
controller.onAcceptToBook();
$httpBackend.flush();
expect(controller.invoiceIn).toEqual(response);
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('InvoiceIn booked');
});
});
describe('checkToBook()', () => {
it(`should show a warning before book`, () => {
controller.$.confirmToBookAnyway = {show: () => {}};
jest.spyOn(controller.$.confirmToBookAnyway, 'show');
const invoceInId = 1;
const data = {
totalDueDay: 'an amount',
totalTaxableBase: 'distinct amount'
};
$httpBackend.expectGET(`InvoiceIns/${invoceInId}/getTotals`).respond(data);
$httpBackend.expectGET(`InvoiceInDueDays/count`).respond();
controller.checkToBook();
$httpBackend.flush();
expect(controller.$.confirmToBookAnyway.show).toHaveBeenCalledWith();
});
it(`should call onAcceptToBook`, () => {
controller.onAcceptToBook = jest.fn();
const invoceInId = 1;
const data = {
totalDueDay: 'same amount',
totalTaxableBase: 'same amount'
};
$httpBackend.expectGET(`InvoiceIns/${invoceInId}/getTotals`).respond(data);
$httpBackend.expectGET(`InvoiceInDueDays/count`).respond();
controller.checkToBook();
$httpBackend.flush();
expect(controller.onAcceptToBook).toHaveBeenCalledWith();
});
});
});

View File

@ -1,13 +1,16 @@
InvoiceIn: Facturas recibidas
Search invoices in by reference: Buscar facturas recibidas por referencia
Entries list: Listado de entradas
InvoiceIn deleted: Factura eliminada
Remove tax: Quitar iva
Add tax: Añadir iva
Amounts do not match: La BI no coincide con el vencimiento ni con el total
Due day: Vencimiento
Entries list: Listado de entradas
Foreign value: Divisa
InvoiceIn: Facturas recibidas
InvoiceIn cloned: Factura clonada
InvoiceIn deleted: Factura eliminada
Invoice list: Listado de facturas recibidas
InvoiceIn booked: Factura contabilizada
Remove tax: Quitar iva
Sage tax: Sage iva
Sage transaction: Sage transaccion
Foreign value: Divisa
Due day: Vencimiento
Invoice list: Listado de facturas recibidas
InvoiceIn cloned: Factura clonada
Search invoices in by reference: Buscar facturas recibidas por referencia
To book: Contabilizar

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('payBack', {
description: 'Create ticket with the selected lines changing the sign to the quantites',
@ -39,6 +41,15 @@ module.exports = Self => {
try {
const salesIds = [];
const params = [];
const userId = ctx.req.accessToken.userId;
const isClaimManager = await Self.app.models.Account.hasRole(userId, 'claimManager');
const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant');
const hasValidRole = isClaimManager || isSalesAssistant;
if (!hasValidRole)
throw new UserError(`You don't have privileges to create pay back`);
sales.forEach(sale => {
salesIds.push(sale.id);
params.push('?');

View File

@ -3,15 +3,17 @@ const models = require('vn-loopback/server/server').models;
describe('sale payBack()', () => {
it('should create ticket with the selected lines changing the sign to the quantites', async() => {
const tx = await models.Sale.beginTransaction({});
const ctx = {req: {accessToken: {userId: 9}}};
const ticketId = 11;
const sales = [
{id: 7, ticketFk: 11},
{id: 8, ticketFk: 11}
];
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}};
const response = await models.Sale.payBack(ctx, sales, ticketId, options);
const [newTicketId] = await models.Sale.rawSql('SELECT MAX(t.id) id FROM vn.ticket t;', null, options);
@ -23,4 +25,30 @@ describe('sale payBack()', () => {
throw e;
}
});
it(`should throw an error if the user doesn't have privileges to create a pay back`, async() => {
const tx = await models.Sale.beginTransaction({});
const ctx = {req: {accessToken: {userId: 1}}};
const ticketId = 11;
const sales = [
{id: 7, ticketFk: 11}
];
let error;
try {
const options = {transaction: tx};
await models.Sale.payBack(ctx, sales, ticketId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual(`You don't have privileges to create pay back`);
});
});

View File

@ -492,7 +492,9 @@
</vn-item>
<vn-item translate
name="payBack"
ng-click="$ctrl.createPayBack()">
ng-click="$ctrl.createPayBack()"
vn-acl="claimManager, salesAssistant"
vn-acl-action="remove">
Pay Back
</vn-item>
</vn-menu>

View File

@ -10,64 +10,68 @@ module.exports = Self => {
accepts: [
{
arg: 'filter',
type: 'Object',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
}, {
arg: 'search',
type: 'String',
type: 'string',
description: 'Searchs the travel by id',
http: {source: 'query'}
}, {
arg: 'id',
type: 'Integer',
type: 'integer',
description: 'The travel id',
http: {source: 'query'}
}, {
arg: 'shippedFrom',
type: 'Date',
type: 'date',
description: 'The shipped from date filter',
http: {source: 'query'}
}, {
arg: 'shippedTo',
type: 'Date',
type: 'date',
description: 'The shipped to date filter',
http: {source: 'query'}
}, {
arg: 'landedFrom',
type: 'Date',
type: 'date',
description: 'The landed from date filter',
http: {source: 'query'}
}, {
arg: 'landedTo',
type: 'Date',
type: 'date',
description: 'The landed to date filter',
http: {source: 'query'}
}, {
arg: 'agencyFk',
type: 'Number',
type: 'number',
description: 'The agencyModeFk id',
http: {source: 'query'}
}, {
arg: 'warehouseOutFk',
type: 'Number',
type: 'number',
description: 'The warehouseOutFk filter',
http: {source: 'query'}
}, {
arg: 'warehouseInFk',
type: 'Number',
type: 'number',
description: 'The warehouseInFk filter',
http: {source: 'query'}
}, {
arg: 'totalEntries',
type: 'Number',
type: 'number',
description: 'The totalEntries filter',
http: {source: 'query'}
}, {
arg: 'ref',
type: 'string',
description: 'The reference'
}
}, {
arg: 'continent',
type: 'string',
description: 'The continent code'
},
],
returns: {
type: ['Object'],
@ -102,6 +106,7 @@ module.exports = Self => {
case 'warehouseOutFk':
case 'warehouseInFk':
case 'totalEntries':
case 'continent':
param = `t.${param}`;
return {[param]: value};
}
@ -129,11 +134,15 @@ module.exports = Self => {
t.totalEntries,
am.name agencyModeName,
win.name warehouseInName,
wout.name warehouseOutName
wout.name warehouseOutName,
cnt.code continent
FROM vn.travel t
JOIN vn.agencyMode am ON am.id = t.agencyFk
JOIN vn.warehouse win ON win.id = t.warehouseInFk
JOIN vn.warehouse wout ON wout.id = t.warehouseOutFk) AS t`
JOIN vn.warehouse wout ON wout.id = t.warehouseOutFk
JOIN warehouse wo ON wo.id = t.warehouseOutFk
JOIN country c ON c.id = wo.countryFk
LEFT JOIN continent cnt ON cnt.id = c.continentFk) AS t`
);
stmt.merge(conn.makeSuffix(filter));

View File

@ -70,4 +70,16 @@ describe('Travel filter()', () => {
expect(result.length).toEqual(1);
});
it('should return the travel matching "continent"', async() => {
const ctx = {
args: {
continent: 'EU',
}
};
const result = await app.models.Travel.filter(ctx);
expect(result.length).toEqual(5);
});
});

View File

@ -93,6 +93,15 @@
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
label="Continent Out"
ng-model="filter.continent"
url="Continents"
show-field="name"
value-field="code">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>

View File

@ -22,7 +22,7 @@
</vn-label-value>
<vn-check
label="Delivered"
value="{{$ctrl.travelData.isDelivered}}"
ng-model="$ctrl.travelData.isDelivered"
disabled="true">
</vn-check>
</vn-one>
@ -37,7 +37,7 @@
</vn-label-value>
<vn-check
label="Received"
value="{{$ctrl.travelData.isReceived}}"
ng-model="$ctrl.travelData.isReceived"
disabled="true">
</vn-check>
</vn-one>

1676
print/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff