diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9ed1c8fc2..899dfc788 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,6 +12,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"cSpell.words": [
- "salix"
+ "salix",
+ "fdescribe"
]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30afaa69b..08a62f044 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2348.01] - 2023-11-30
### Added
+- (Ticket -> Adelantar) Permite mover lineas sin generar negativos
+- (Ticket -> Adelantar) Permite modificar la fecha de los tickets
+
### Changed
### Fixed
+- (Ticket -> RocketChat) Arreglada detección de cambios
+
## [2346.01] - 2023-11-16
diff --git a/README.md b/README.md
index 59f5dbcf7..b420bc44f 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
-* Node.js >= 16.x LTS
+* Node.js
* Docker
* Git
@@ -17,20 +17,7 @@ You will need to install globally the following items.
$ sudo npm install -g jest gulp-cli
```
-For the usage of jest --watch on macOs.
-```
-$ brew install watchman
-```
-* [watchman](https://facebook.github.io/watchman/)
-
-## Linux Only Prerequisites
-
-Your user must be on the docker group to use it so you will need to run this command:
-```
-$ sudo usermod -a -G docker yourusername
-```
-
-## Getting Started // Installing
+## Installing dependencies and launching
Pull from repository.
@@ -76,29 +63,6 @@ In Visual Studio Code we use the ESLint extension.
ext install dbaeumer.vscode-eslint
```
-Gitlens for visualization of code authorship
-```
-ext install eamodio.gitlens
-```
-
-Spanish language pack
-```
-ext install ms-ceintl.vscode-language-pack-es
-```
-
-### Recommended extensions
-
-Material icon Theme
-```
-ext install pkief.material-icon-theme
-```
-
-Material UI Themes
-```
-ext install equinusocio.vsc-material-theme
-```
-
-
## Built With
* [angularjs](https://angularjs.org/)
diff --git a/back/methods/vn-user/sign-in.js b/back/methods/vn-user/sign-in.js
index b9e0d2f70..25f708b8e 100644
--- a/back/methods/vn-user/sign-in.js
+++ b/back/methods/vn-user/sign-in.js
@@ -49,8 +49,13 @@ module.exports = Self => {
if (vnUser.twoFactor)
throw new ForbiddenError(null, 'REQUIRES_2FA');
}
-
- return Self.validateLogin(user, password);
+ const validateLogin = await Self.validateLogin(user, password);
+ await Self.app.models.SignInLog.create({
+ id: validateLogin.token,
+ userFk: vnUser.id,
+ ip: ctx.req.ip
+ });
+ return validateLogin;
};
Self.passExpired = async vnUser => {
diff --git a/back/models/vn-user.js b/back/models/vn-user.js
index de5bf7b63..5845c2192 100644
--- a/back/models/vn-user.js
+++ b/back/models/vn-user.js
@@ -2,6 +2,7 @@ const vnModel = require('vn-loopback/common/models/vn-model');
const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
const LoopBackContext = require('loopback-context');
+const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
vnModel(Self);
@@ -92,7 +93,11 @@ module.exports = function(Self) {
};
Self.on('resetPasswordRequest', async function(info) {
- const url = await Self.app.models.Url.getUrl();
+ const loopBackContext = LoopBackContext.getCurrentContext();
+ const httpCtx = {req: loopBackContext.active};
+ const httpRequest = httpCtx.req.http.req;
+ const headers = httpRequest.headers;
+ const origin = headers.origin;
const defaultHash = '/reset-password?access_token=$token$';
const recoverHashes = {
@@ -108,7 +113,7 @@ module.exports = function(Self) {
const params = {
recipient: info.email,
lang: user.lang,
- url: url.slice(0, -1) + recoverHash
+ url: origin + '/#!' + recoverHash
};
const options = Object.assign({}, info.options);
@@ -121,10 +126,16 @@ module.exports = function(Self) {
});
Self.validateLogin = async function(user, password) {
- let loginInfo = Object.assign({password}, Self.userUses(user));
- token = await Self.login(loginInfo, 'user');
+ const loginInfo = Object.assign({password}, Self.userUses(user));
+ const token = await Self.login(loginInfo, 'user');
const userToken = await token.user.get();
+
+ if (userToken.username.toLowerCase() !== user.toLowerCase()) {
+ console.error('ERROR!!! - Signin with other user', userToken, user);
+ throw new UserError('Try again');
+ }
+
try {
await Self.app.models.Account.sync(userToken.name, password);
} catch (err) {
@@ -220,8 +231,11 @@ module.exports = function(Self) {
class Mailer {
async send(verifyOptions, cb) {
+ const url = new URL(verifyOptions.verifyHref);
+ if (process.env.NODE_ENV) url.port = '';
+
const params = {
- url: verifyOptions.verifyHref,
+ url: url.href,
recipient: verifyOptions.to
};
diff --git a/db/.archive/225201/00-invoiceOut_new.sql b/db/.archive/225201/00-invoiceOut_new.sql
index 4c60b50bc..8e23fb43b 100644
--- a/db/.archive/225201/00-invoiceOut_new.sql
+++ b/db/.archive/225201/00-invoiceOut_new.sql
@@ -74,7 +74,7 @@ BEGIN
clientFk,
dued,
companyFk,
- cplusInvoiceType477Fk
+ siiTypeInvoiceOutFk
)
SELECT
1,
diff --git a/db/.archive/231001/02-invoiceOut_new.sql b/db/.archive/231001/02-invoiceOut_new.sql
index d2b96eff7..d570dfb72 100644
--- a/db/.archive/231001/02-invoiceOut_new.sql
+++ b/db/.archive/231001/02-invoiceOut_new.sql
@@ -96,7 +96,7 @@ BEGIN
clientFk,
dued,
companyFk,
- cplusInvoiceType477Fk
+ siiTypeInvoiceOutFk
)
SELECT
1,
diff --git a/db/.archive/232001/00-invoiceOut_new.sql b/db/.archive/232001/00-invoiceOut_new.sql
index b4fc5c824..b497dffda 100644
--- a/db/.archive/232001/00-invoiceOut_new.sql
+++ b/db/.archive/232001/00-invoiceOut_new.sql
@@ -96,7 +96,7 @@ BEGIN
clientFk,
dued,
companyFk,
- cplusInvoiceType477Fk
+ siiTypeInvoiceOutFk
)
SELECT
1,
diff --git a/db/changes/234601/00-transferInvoice.sql b/db/changes/234601/00-transferInvoice.sql
index 7a9890ae4..d9ae39464 100644
--- a/db/changes/234601/00-transferInvoice.sql
+++ b/db/changes/234601/00-transferInvoice.sql
@@ -1,6 +1,6 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
- ('CplusInvoiceType477', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
+ ('SiiTypeInvoiceOut', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative');
diff --git a/db/changes/234602/00-roleSync.sql b/db/changes/234602/00-roleSync.sql
new file mode 100644
index 000000000..7ce277748
--- /dev/null
+++ b/db/changes/234602/00-roleSync.sql
@@ -0,0 +1 @@
+CALL `account`.`role_sync`();
diff --git a/db/changes/234603/00-createSignInLogTable.sql b/db/changes/234603/00-createSignInLogTable.sql
new file mode 100644
index 000000000..977de4646
--- /dev/null
+++ b/db/changes/234603/00-createSignInLogTable.sql
@@ -0,0 +1,19 @@
+
+
+--
+-- Table structure for table `signInLog`
+--
+
+DROP TABLE IF EXISTS `account`.`signInLog`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `account`.`signInLog` (
+ `id` varchar(10) NOT NULL ,
+ `userFk` int(10) unsigned DEFAULT NULL,
+ `creationDate` timestamp NULL DEFAULT current_timestamp(),
+ `ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `userFk` (`userFk`),
+ CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+);
+
diff --git a/db/changes/234801/00-ACL_executeRoutine.sql b/db/changes/234801/00-ACL_executeRoutine.sql
new file mode 100644
index 000000000..cfe7018e9
--- /dev/null
+++ b/db/changes/234801/00-ACL_executeRoutine.sql
@@ -0,0 +1,4 @@
+INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
+ VALUES
+ ('Application', 'executeProc', '*', 'ALLOW', 'ROLE', 'employee'),
+ ('Application', 'executeFunc', '*', 'ALLOW', 'ROLE', 'employee');
diff --git a/db/changes/234801/00-ticket_canAdvance_update.sql b/db/changes/234801/00-ticket_canAdvance_update.sql
new file mode 100644
index 000000000..afe0a4dc6
--- /dev/null
+++ b/db/changes/234801/00-ticket_canAdvance_update.sql
@@ -0,0 +1,152 @@
+DELIMITER $$
+$$
+CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT)
+BEGIN
+/**
+ * Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar.
+ *
+ * @param vDateFuture Fecha de los tickets que se quieren adelantar.
+ * @param vDateToAdvance Fecha a cuando se quiere adelantar.
+ * @param vWarehouseFk Almacén
+ */
+ DECLARE vDateInventory DATE;
+
+ SELECT inventoried INTO vDateInventory FROM config;
+
+ CREATE OR REPLACE TEMPORARY TABLE tmp.stock
+ (itemFk INT PRIMARY KEY,
+ amount INT)
+ ENGINE = MEMORY;
+
+ INSERT INTO tmp.stock(itemFk, amount)
+ SELECT itemFk, SUM(quantity) amount FROM
+ (
+ SELECT itemFk, quantity
+ FROM itemTicketOut
+ WHERE shipped >= vDateInventory
+ AND shipped < vDateFuture
+ AND warehouseFk = vWarehouseFk
+ UNION ALL
+ SELECT itemFk, quantity
+ FROM itemEntryIn
+ WHERE landed >= vDateInventory
+ AND landed <= vDateToAdvance
+ AND isVirtualStock = FALSE
+ AND warehouseInFk = vWarehouseFk
+ UNION ALL
+ SELECT itemFk, quantity
+ FROM itemEntryOut
+ WHERE shipped >= vDateInventory
+ AND shipped < vDateFuture
+ AND warehouseOutFk = vWarehouseFk
+ ) t
+ GROUP BY itemFk HAVING amount != 0;
+
+ CREATE OR REPLACE TEMPORARY TABLE tmp.filter
+ (INDEX (id))
+ SELECT
+ origin.ticketFk futureId,
+ dest.ticketFk id,
+ dest.state,
+ origin.futureState,
+ origin.futureIpt,
+ dest.ipt,
+ origin.workerFk,
+ origin.futureLiters,
+ origin.futureLines,
+ dest.shipped,
+ origin.shipped futureShipped,
+ dest.totalWithVat,
+ origin.totalWithVat futureTotalWithVat,
+ dest.agency,
+ dest.agencyModeFk,
+ origin.futureAgency,
+ origin.agencyModeFk futureAgencyModeFk,
+ dest.lines,
+ dest.liters,
+ origin.futureLines - origin.hasStock AS notMovableLines,
+ (origin.futureLines = origin.hasStock) AS isFullMovable,
+ dest.zoneFk,
+ origin.futureZoneFk,
+ origin.futureZoneName,
+ origin.classColor futureClassColor,
+ dest.classColor,
+ origin.clientFk futureClientFk,
+ origin.addressFk futureAddressFk,
+ origin.warehouseFk futureWarehouseFk,
+ origin.companyFk futureCompanyFk,
+ IFNULL(dest.nickname, origin.nickname) nickname,
+ dest.landed
+ FROM (
+ SELECT
+ s.ticketFk,
+ c.salesPersonFk workerFk,
+ t.shipped,
+ t.totalWithVat,
+ st.name futureState,
+ am.name futureAgency,
+ count(s.id) futureLines,
+ GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt,
+ CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters,
+ SUM((s.quantity <= IFNULL(st.amount,0))) hasStock,
+ z.id futureZoneFk,
+ z.name futureZoneName,
+ st.classColor,
+ t.clientFk,
+ t.nickname,
+ t.addressFk,
+ t.warehouseFk,
+ t.companyFk,
+ t.agencyModeFk
+ FROM ticket t
+ JOIN client c ON c.id = t.clientFk
+ JOIN sale s ON s.ticketFk = t.id
+ JOIN saleVolume sv ON sv.saleFk = s.id
+ JOIN item i ON i.id = s.itemFk
+ JOIN ticketState ts ON ts.ticketFk = t.id
+ JOIN state st ON st.id = ts.stateFk
+ JOIN agencyMode am ON t.agencyModeFk = am.id
+ JOIN zone z ON t.zoneFk = z.id
+ LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
+ LEFT JOIN tmp.stock st ON st.itemFk = i.id
+ WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture)
+ AND t.warehouseFk = vWarehouseFk
+ GROUP BY t.id
+ ) origin
+ LEFT JOIN (
+ SELECT
+ t.id ticketFk,
+ st.name state,
+ GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt,
+ t.shipped,
+ t.totalWithVat,
+ am.name agency,
+ CAST(SUM(litros) AS DECIMAL(10,0)) liters,
+ CAST(COUNT(*) AS DECIMAL(10,0)) `lines`,
+ st.classColor,
+ t.clientFk,
+ t.nickname,
+ t.addressFk,
+ t.zoneFk,
+ t.warehouseFk,
+ t.companyFk,
+ t.landed,
+ t.agencyModeFk
+ FROM ticket t
+ JOIN sale s ON s.ticketFk = t.id
+ JOIN saleVolume sv ON sv.saleFk = s.id
+ JOIN item i ON i.id = s.itemFk
+ JOIN ticketState ts ON ts.ticketFk = t.id
+ JOIN state st ON st.id = ts.stateFk
+ JOIN agencyMode am ON t.agencyModeFk = am.id
+ LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
+ WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance)
+ AND t.warehouseFk = vWarehouseFk
+ AND st.order <= 5
+ GROUP BY t.id
+ ) dest ON dest.addressFk = origin.addressFk
+ WHERE origin.hasStock;
+
+ DROP TEMPORARY TABLE tmp.stock;
+END$$
+DELIMITER ;
diff --git a/db/dump/dumpedFixtures.sql b/db/dump/dumpedFixtures.sql
index 2e1511b59..43eba7f24 100644
--- a/db/dump/dumpedFixtures.sql
+++ b/db/dump/dumpedFixtures.sql
@@ -275,13 +275,13 @@ INSERT INTO `cplusInvoiceType472` VALUES (1,'F1 - Factura'),(2,'F2 - Factura sim
UNLOCK TABLES;
--
--- Dumping data for table `cplusInvoiceType477`
+-- Dumping data for table `siiTypeInvoiceOut`
--
-LOCK TABLES `cplusInvoiceType477` WRITE;
-/*!40000 ALTER TABLE `cplusInvoiceType477` DISABLE KEYS */;
-INSERT INTO `cplusInvoiceType477` VALUES (1,'F1 - Factura'),(2,'F2 - Factura simplificada (ticket)'),(3,'F3 - Factura emitida en sustitución de facturas simplificadas facturadas y declaradas'),(4,'F4 - Asiento resumen de facturas'),(5,'R1 - Factura rectificativa (Art. 80.1, 80.2 y error fundado en derecho)'),(6,'R2 - Factura rectificativa (Art. 80.3)'),(7,'R3 - Factura rectificativa (Art. 80.4)'),(8,'R4 - Factura rectificativa (Resto)'),(9,'R5 - Factura rectificativa en facturas simplificadas');
-/*!40000 ALTER TABLE `cplusInvoiceType477` ENABLE KEYS */;
+LOCK TABLES `siiTypeInvoiceOut` WRITE;
+/*!40000 ALTER TABLE `siiTypeInvoiceOut` DISABLE KEYS */;
+INSERT INTO `siiTypeInvoiceOut` VALUES (1,'F1 - Factura'),(2,'F2 - Factura simplificada (ticket)'),(3,'F3 - Factura emitida en sustitución de facturas simplificadas facturadas y declaradas'),(4,'F4 - Asiento resumen de facturas'),(5,'R1 - Factura rectificativa (Art. 80.1, 80.2 y error fundado en derecho)'),(6,'R2 - Factura rectificativa (Art. 80.3)'),(7,'R3 - Factura rectificativa (Art. 80.4)'),(8,'R4 - Factura rectificativa (Resto)'),(9,'R5 - Factura rectificativa en facturas simplificadas');
+/*!40000 ALTER TABLE `siiTypeInvoiceOut` ENABLE KEYS */;
UNLOCK TABLES;
--
diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql
index 499336243..a3791f9f5 100644
--- a/db/dump/fixtures.sql
+++ b/db/dump/fixtures.sql
@@ -367,7 +367,7 @@ INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city
(1105, 'Max Eisenhardt', '251628698', 'MAGNETO', 'Rogue', 'UNKNOWN WHEREABOUTS', 'Gotham', 46460, 1111111111, 222222222, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 300, 8, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1, 'florist','normal'),
(1106, 'DavidCharlesHaller', '53136686Q', 'LEGION', 'Charles Xavier', 'CITY OF NEW YORK, NEW YORK, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
(1107, 'Hank Pym', '09854837G', 'ANT MAN', 'Hawk', 'ANTHILL, SAN FRANCISCO, CALIFORNIA', 'Gotham', 46460, 1111111111, 222222222, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
- (1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
+ (1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 5, 1, 300, 13, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
(1109, 'Bruce Banner', '16104829E', 'HULK', 'Black widow', 'SOMEWHERE IN NEW YORK', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 9, 0, 1, 'florist','normal'),
(1110, 'Jessica Jones', '58282869H', 'JESSICA JONES', 'Luke Cage', 'NYCC 2015 POSTER', 'Gotham', 46460, 1111111111, 222222222, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1, 'florist','normal'),
(1111, 'Missing', NULL, 'MISSING MAN', 'Anton', 'THE SPACE, UNIVERSE FAR AWAY', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, 0, 1, 0, NULL, 1, 0, NULL, 0, 1, 'others','normal'),
@@ -405,7 +405,7 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(5, 'Max Eisenhardt', 'Unknown Whereabouts', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 1),
(6, 'DavidCharlesHaller', 'Evil hideout', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 1),
(7, 'Hank Pym', 'Anthill', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 1),
- (8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1),
+ (8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 5, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1),
(9, 'Bruce Banner', 'Somewhere in New York', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 1),
(10, 'Jessica Jones', 'NYCC 2015 Poster', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 1),
(11, 'Missing', 'The space', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1111, 10, NULL, NULL, 0, 1),
@@ -437,7 +437,7 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(125, 'The plastic cell', 'address 25', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 0),
(126, 'Many places', 'address 26', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 0),
(127, 'Your pocket', 'address 27', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 0),
- (128, 'Cerebro', 'address 28', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
+ (128, 'Cerebro', 'address 28', 'Gotham', 46460, 5, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Gotham', 'EC170150', 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 0),
(130, 'Non valid address', 'address 30', 'Gotham', 46460, 1, 1111111111, 222222222, 0, 1101, 2, NULL, NULL, 0, 0);
diff --git a/db/dump/structure.sql b/db/dump/structure.sql
index b242821fc..3d6156580 100644
--- a/db/dump/structure.sql
+++ b/db/dump/structure.sql
@@ -2352,6 +2352,90 @@ BEGIN
END IF;
END ;;
DELIMITER ;
+
+
+DELIMITER ;;
+CREATE DEFINER=`root`@`localhost` FUNCTION `account`.`user_hasRoutinePriv`(vType ENUM('PROCEDURE', 'FUNCTION'),
+ vChain VARCHAR(100),
+ vUserFk INT
+) RETURNS tinyint(1)
+ READS SQL DATA
+BEGIN
+
+/**
+ * Search if the user has privileges on routines.
+ *
+ * @param vType procedure or function
+ * @param vChain string passed with this syntax dbName.tableName
+ * @param vUserFk user to ckeck
+ * @return vHasPrivilege
+ */
+ DECLARE vHasPrivilege BOOL DEFAULT FALSE;
+ DECLARE vDb VARCHAR(50);
+ DECLARE vObject VARCHAR(50);
+ DECLARE vChainExists BOOL;
+ DECLARE vExecutePriv INT DEFAULT 262144;
+ -- 262144 = CONV(1000000000000000000, 2, 10)
+ -- 1000000000000000000 execution permission expressed in binary base
+
+ SET vDb = SUBSTRING_INDEX(vChain, '.', 1);
+ SET vChain = SUBSTRING(vChain, LENGTH(vDb) + 2);
+ SET vObject = SUBSTRING_INDEX(vChain, '.', 1);
+
+ SELECT COUNT(*) INTO vChainExists
+ FROM mysql.proc
+ WHERE db = vDb
+ AND `name` = vObject
+ AND `type` = vType
+ LIMIT 1;
+
+ IF NOT vChainExists THEN
+ RETURN FALSE;
+ END IF;
+
+ DROP TEMPORARY TABLE IF EXISTS tRole;
+ CREATE TEMPORARY TABLE tRole
+ (INDEX (`name`))
+ ENGINE = MEMORY
+ SELECT r.`name`
+ FROM user u
+ JOIN roleRole rr ON rr.role = u.role
+ JOIN `role` r ON r.id = rr.inheritsFrom
+ WHERE u.id = vUserFk;
+
+ SELECT TRUE INTO vHasPrivilege
+ FROM mysql.global_priv gp
+ JOIN tRole tr ON tr.name = gp.`User`
+ OR CONCAT('$', tr.name) = gp.`User`
+ WHERE JSON_VALUE(gp.Priv, '$.access') >= vExecutePriv
+ AND gp.Host = ''
+ LIMIT 1;
+
+ IF NOT vHasPrivilege THEN
+ SELECT TRUE INTO vHasPrivilege
+ FROM mysql.db db
+ JOIN tRole tr ON tr.name = db.`User`
+ WHERE db.Db = vDb
+ AND db.Execute_priv = 'Y';
+ END IF;
+
+ IF NOT vHasPrivilege THEN
+ SELECT TRUE INTO vHasPrivilege
+ FROM mysql.procs_priv pp
+ JOIN tRole tr ON tr.name = pp.`User`
+ WHERE pp.Db = vDb
+ AND pp.Routine_name = vObject
+ AND pp.Routine_type = vType
+ AND pp.Proc_priv = 'Execute'
+ LIMIT 1;
+ END IF;
+
+ DROP TEMPORARY TABLE tRole;
+ RETURN vHasPrivilege;
+END ;;
+DELIMITER ;
+
+
/*!50003 SET sql_mode = @saved_sql_mode */ ;
/*!50003 SET character_set_client = @saved_cs_client */ ;
/*!50003 SET character_set_results = @saved_cs_results */ ;
@@ -25993,13 +26077,13 @@ CREATE TABLE `cplusInvoiceType472` (
/*!40101 SET character_set_client = @saved_cs_client */;
--
--- Table structure for table `cplusInvoiceType477`
+-- Table structure for table `siiTypeInvoiceOut`
--
-DROP TABLE IF EXISTS `cplusInvoiceType477`;
+DROP TABLE IF EXISTS `siiTypeInvoiceOut`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
-CREATE TABLE `cplusInvoiceType477` (
+CREATE TABLE `siiTypeInvoiceOut` (
`id` int(10) unsigned NOT NULL,
`description` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
PRIMARY KEY (`id`)
@@ -29450,16 +29534,16 @@ CREATE TABLE `invoiceCorrection` (
`correctingFk` int(10) unsigned NOT NULL COMMENT 'Factura rectificativa',
`correctedFk` int(10) unsigned NOT NULL COMMENT 'Factura rectificada',
`cplusRectificationTypeFk` int(10) unsigned NOT NULL,
- `cplusInvoiceType477Fk` int(10) unsigned NOT NULL,
+ `siiTypeInvoiceOutFk` int(10) unsigned NOT NULL,
`invoiceCorrectionTypeFk` int(11) NOT NULL DEFAULT 3,
PRIMARY KEY (`correctingFk`),
KEY `correctedFk_idx` (`correctedFk`),
KEY `invoiceCorrection_ibfk_1_idx` (`cplusRectificationTypeFk`),
- KEY `cplusInvoiceTyoeFk_idx` (`cplusInvoiceType477Fk`),
+ KEY `cplusInvoiceTyoeFk_idx` (`siiTypeInvoiceOutFk`),
KEY `invoiceCorrectionTypeFk_idx` (`invoiceCorrectionTypeFk`),
CONSTRAINT `corrected_fk` FOREIGN KEY (`correctedFk`) REFERENCES `invoiceOut` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `correcting_fk` FOREIGN KEY (`correctingFk`) REFERENCES `invoiceOut` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
- CONSTRAINT `cplusInvoiceTyoeFk` FOREIGN KEY (`cplusInvoiceType477Fk`) REFERENCES `cplusInvoiceType477` (`id`) ON UPDATE CASCADE,
+ CONSTRAINT `cplusInvoiceTyoeFk` FOREIGN KEY (`siiTypeInvoiceOutFk`) REFERENCES `siiTypeInvoiceOut` (`id`) ON UPDATE CASCADE,
CONSTRAINT `invoiceCorrectionType_Fk33` FOREIGN KEY (`invoiceCorrectionTypeFk`) REFERENCES `invoiceCorrectionType` (`id`) ON UPDATE CASCADE,
CONSTRAINT `invoiceCorrection_ibfk_1` FOREIGN KEY (`cplusRectificationTypeFk`) REFERENCES `cplusRectificationType` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci COMMENT='Relacion entre las facturas rectificativas y las rectificadas.';
@@ -30130,7 +30214,7 @@ CREATE TABLE `invoiceOut` (
`companyFk` int(10) unsigned NOT NULL DEFAULT 442,
`hasPdf` tinyint(3) unsigned NOT NULL DEFAULT 0,
`booked` date DEFAULT NULL,
- `cplusInvoiceType477Fk` int(10) unsigned NOT NULL DEFAULT 1,
+ `siiTypeInvoiceOutFk` int(10) unsigned NOT NULL DEFAULT 1,
`cplusTaxBreakFk` int(10) unsigned NOT NULL DEFAULT 1,
`cplusSubjectOpFk` int(10) unsigned NOT NULL DEFAULT 1,
`cplusTrascendency477Fk` int(10) unsigned NOT NULL DEFAULT 1,
@@ -30140,13 +30224,13 @@ CREATE TABLE `invoiceOut` (
KEY `Id_Cliente` (`clientFk`),
KEY `empresa_id` (`companyFk`),
KEY `Fecha` (`issued`),
- KEY `Facturas_ibfk_2_idx` (`cplusInvoiceType477Fk`),
+ KEY `Facturas_ibfk_2_idx` (`siiTypeInvoiceOutFk`),
KEY `Facturas_ibfk_3_idx` (`cplusSubjectOpFk`),
KEY `Facturas_ibfk_4_idx` (`cplusTaxBreakFk`),
KEY `Facturas_ibfk_5_idx` (`cplusTrascendency477Fk`),
KEY `Facturas_idx_Vencimiento` (`dued`),
KEY `invoiceOut_serial` (`serial`),
- CONSTRAINT `invoiceOut_ibfk_2` FOREIGN KEY (`cplusInvoiceType477Fk`) REFERENCES `cplusInvoiceType477` (`id`) ON UPDATE CASCADE,
+ CONSTRAINT `invoiceOut_ibfk_2` FOREIGN KEY (`siiTypeInvoiceOutFk`) REFERENCES `siiTypeInvoiceOut` (`id`) ON UPDATE CASCADE,
CONSTRAINT `invoiceOut_ibfk_3` FOREIGN KEY (`cplusSubjectOpFk`) REFERENCES `cplusSubjectOp` (`id`) ON UPDATE CASCADE,
CONSTRAINT `invoiceOut_ibfk_4` FOREIGN KEY (`cplusTaxBreakFk`) REFERENCES `cplusTaxBreak` (`id`) ON UPDATE CASCADE,
CONSTRAINT `invoiceOut_serial` FOREIGN KEY (`serial`) REFERENCES `invoiceOutSerial` (`code`),
@@ -30308,7 +30392,7 @@ CREATE TABLE `invoiceOutSerial` (
`isTaxed` tinyint(1) NOT NULL DEFAULT 1,
`taxAreaFk` varchar(15) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT 'NATIONAL',
`isCEE` tinyint(1) NOT NULL DEFAULT 0,
- `cplusInvoiceType477Fk` int(10) unsigned DEFAULT 1,
+ `siiTypeInvoiceOutFk` int(10) unsigned DEFAULT 1,
`footNotes` longtext DEFAULT NULL,
`isRefEditable` tinyint(4) NOT NULL DEFAULT 0,
`type` enum('global','quick') DEFAULT NULL,
@@ -58288,7 +58372,7 @@ BEGIN
io.cplusTrascendency477Fk AS TIPOCLAVE,
io.cplusTaxBreakFk AS TIPOEXENCI,
io.cplusSubjectOpFk AS TIPONOSUJE,
- io.cplusInvoiceType477Fk AS TIPOFACT,
+ io.siiTypeInvoiceOutFk AS TIPOFACT,
ic.cplusRectificationTypeFk AS TIPORECTIF,
io.companyFk,
RIGHT(io.ref, LENGTH(io.ref) - 1) AS invoiceNum,
@@ -58868,7 +58952,7 @@ BEGIN
clientFk,
dued,
companyFk,
- cplusInvoiceType477Fk
+ siiTypeInvoiceOutFk
)
SELECT
1,
diff --git a/db/export-data.sh b/db/export-data.sh
index 7d346b235..a516992d3 100755
--- a/db/export-data.sh
+++ b/db/export-data.sh
@@ -46,7 +46,7 @@ TABLES=(
bookingPlanner
businessType
cplusInvoiceType472
- cplusInvoiceType477
+ siiTypeInvoiceOut
cplusRectificationType
cplusSubjectOp
cplusTaxBreak
diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js
index cec0545a0..b5dbd42ac 100644
--- a/e2e/helpers/selectors.js
+++ b/e2e/helpers/selectors.js
@@ -722,7 +722,7 @@ export default {
isFullMovable: 'vn-check[ng-model="filter.isFullMovable"]',
warehouseFk: 'vn-autocomplete[label="Warehouse"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
- moveButton: 'vn-button[vn-tooltip="Advance tickets"]',
+ moveButton: 'vn-button[vn-tooltip="Advance tickets with negatives"]',
acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
tableId: 'vn-textfield[name="id"]',
diff --git a/loopback/common/methods/application/execute.js b/loopback/common/methods/application/execute.js
new file mode 100644
index 000000000..a468dcd70
--- /dev/null
+++ b/loopback/common/methods/application/execute.js
@@ -0,0 +1,28 @@
+const UserError = require('vn-loopback/util/user-error');
+
+module.exports = Self => {
+ Self.execute = async(ctx, type, query, params, options) => {
+ const userId = ctx.req.accessToken.userId;
+ const models = Self.app.models;
+ params = params ?? [];
+
+ const myOptions = {userId: ctx.req.accessToken.userId};
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
+
+ const chain = query.split(' ')[1];
+
+ const [canExecute] = await models.ProcsPriv.rawSql(
+ 'SELECT account.user_hasRoutinePriv(?,?,?)',
+ [type, chain, userId],
+ myOptions);
+
+ if (!Object.values(canExecute)[0]) throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
+
+ const argString = params.map(() => '?').join(',');
+
+ const response = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
+ if (!Array.isArray(response)) return;
+ return response[0];
+ };
+};
diff --git a/loopback/common/methods/application/executeFunc.js b/loopback/common/methods/application/executeFunc.js
new file mode 100644
index 000000000..a42fdae67
--- /dev/null
+++ b/loopback/common/methods/application/executeFunc.js
@@ -0,0 +1,41 @@
+module.exports = Self => {
+ Self.remoteMethodCtx('executeFunc', {
+ description: 'Return result of function',
+ accessType: 'EXECUTE',
+ accepts: [
+ {
+ arg: 'routine',
+ type: 'string',
+ description: 'The routine name',
+ required: true,
+ http: {source: 'path'}
+ },
+ {
+ arg: 'schema',
+ type: 'string',
+ description: 'The routine schema',
+ required: true,
+ },
+ {
+ arg: 'params',
+ type: ['any'],
+ description: 'The params array',
+ },
+ ],
+ returns: {
+ type: 'any',
+ root: true
+ },
+ http: {
+ path: `/:routine/execute-func`,
+ verb: 'POST'
+ }
+ });
+
+ Self.executeFunc = async(ctx, routine, schema, params, options) => {
+ const query = `SELECT ${schema}.${routine}`;
+
+ const response = await Self.execute(ctx, 'FUNCTION', query, params, options);
+ return Object.values(response)[0];
+ };
+};
diff --git a/loopback/common/methods/application/executeProc.js b/loopback/common/methods/application/executeProc.js
new file mode 100644
index 000000000..a8825da0f
--- /dev/null
+++ b/loopback/common/methods/application/executeProc.js
@@ -0,0 +1,39 @@
+module.exports = Self => {
+ Self.remoteMethodCtx('executeProc', {
+ description: 'Return result of procedure',
+ accessType: 'EXECUTE',
+ accepts: [
+ {
+ arg: 'routine',
+ type: 'string',
+ description: 'The routine name',
+ required: true,
+ http: {source: 'path'}
+ },
+ {
+ arg: 'schema',
+ type: 'string',
+ description: 'The routine schema',
+ required: true,
+ },
+ {
+ arg: 'params',
+ type: ['any'],
+ description: 'The params array',
+ },
+ ],
+ returns: {
+ type: 'any',
+ root: true
+ },
+ http: {
+ path: `/:routine/execute-proc`,
+ verb: 'POST'
+ }
+ });
+
+ Self.executeProc = async(ctx, routine, schema, params, options) => {
+ const query = `CALL ${schema}.${routine}`;
+ return Self.execute(ctx, 'PROCEDURE', query, params, options);
+ };
+};
diff --git a/loopback/common/methods/application/spec/execute.spec.js b/loopback/common/methods/application/spec/execute.spec.js
new file mode 100644
index 000000000..1a0a8ace9
--- /dev/null
+++ b/loopback/common/methods/application/spec/execute.spec.js
@@ -0,0 +1,161 @@
+const models = require('vn-loopback/server/server').models;
+
+describe('Application execute()/executeProc()/executeFunc()', () => {
+ const userWithoutPrivileges = 1;
+ const userWithPrivileges = 9;
+ const userWithInheritedPrivileges = 120;
+ let tx;
+
+ function getCtx(userId) {
+ return {
+ req: {
+ accessToken: {userId},
+ headers: {origin: 'http://localhost'}
+ }
+ };
+ }
+
+ beforeEach(async() => {
+ tx = await models.Application.beginTransaction({});
+ const options = {transaction: tx};
+
+ await models.Application.rawSql(`
+ CREATE OR REPLACE PROCEDURE vn.myProcedure(vMyParam INT)
+ BEGIN
+ SELECT vMyParam myParam, t.*
+ FROM ticket t
+ LIMIT 2;
+ END
+ `, null, options);
+
+ await models.Application.rawSql(`
+ CREATE OR REPLACE FUNCTION bs.myFunction(vMyParam INT) RETURNS int(11)
+ BEGIN
+ RETURN vMyParam;
+ END
+ `, null, options);
+
+ await models.Application.rawSql(`
+ GRANT EXECUTE ON PROCEDURE vn.myProcedure TO developer;
+ GRANT EXECUTE ON FUNCTION bs.myFunction TO developer;
+ `, null, options);
+ });
+
+ it('should throw error when execute procedure and not have privileges', async() => {
+ const ctx = getCtx(userWithoutPrivileges);
+
+ let error;
+ try {
+ const options = {transaction: tx};
+
+ await models.Application.execute(
+ ctx,
+ 'PROCEDURE',
+ 'CALL vn.myProcedure',
+ [1],
+ options
+ );
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ error = e;
+ }
+
+ expect(error.message).toEqual(`You don't have enough privileges`);
+ });
+
+ it('should execute procedure and get data', async() => {
+ const ctx = getCtx(userWithPrivileges);
+ try {
+ const options = {transaction: tx};
+
+ const response = await models.Application.execute(
+ ctx,
+ 'PROCEDURE',
+ 'CALL vn.myProcedure',
+ [1],
+ options
+ );
+
+ expect(response.length).toEqual(2);
+ expect(response[0].myParam).toEqual(1);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+
+ describe('Application executeProc()', () => {
+ it('should execute procedure and get data (executeProc)', async() => {
+ const ctx = getCtx(userWithPrivileges);
+ try {
+ const options = {transaction: tx};
+
+ const response = await models.Application.executeProc(
+ ctx,
+ 'myProcedure',
+ 'vn',
+ [1],
+ options
+ );
+
+ expect(response.length).toEqual(2);
+ expect(response[0].myParam).toEqual(1);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+ });
+
+ describe('Application executeFunc()', () => {
+ it('should execute function and get data', async() => {
+ const ctx = getCtx(userWithPrivileges);
+ try {
+ const options = {transaction: tx};
+
+ const response = await models.Application.executeFunc(
+ ctx,
+ 'myFunction',
+ 'bs',
+ [1],
+ options
+ );
+
+ expect(response).toEqual(1);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+
+ it('should execute function and get data with user with inherited privileges', async() => {
+ const ctx = getCtx(userWithInheritedPrivileges);
+ try {
+ const options = {transaction: tx};
+
+ const response = await models.Application.executeFunc(
+ ctx,
+ 'myFunction',
+ 'bs',
+ [1],
+ options
+ );
+
+ expect(response).toEqual(1);
+
+ await tx.rollback();
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ });
+ });
+});
diff --git a/loopback/common/models/application.js b/loopback/common/models/application.js
index 5e767fdc1..ac8ae78f0 100644
--- a/loopback/common/models/application.js
+++ b/loopback/common/models/application.js
@@ -2,4 +2,7 @@
module.exports = function(Self) {
require('../methods/application/status')(Self);
require('../methods/application/post')(Self);
+ require('../methods/application/execute')(Self);
+ require('../methods/application/executeProc')(Self);
+ require('../methods/application/executeFunc')(Self);
};
diff --git a/loopback/common/models/procs-priv.json b/loopback/common/models/procs-priv.json
new file mode 100644
index 000000000..25221d586
--- /dev/null
+++ b/loopback/common/models/procs-priv.json
@@ -0,0 +1,44 @@
+{
+ "name": "ProcsPriv",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "mysql.procs_priv"
+ }
+ },
+ "properties": {
+ "name": {
+ "id": 1,
+ "type": "string",
+ "mysql": {
+ "columnName": "Routine_name"
+ }
+ },
+ "schema": {
+ "id": 3,
+ "type": "string",
+ "mysql": {
+ "columnName": "Db"
+ }
+ },
+ "role": {
+ "type": "string",
+ "mysql": {
+ "columnName": "user"
+ }
+ },
+ "type": {
+ "id": 2,
+ "type": "string",
+ "mysql": {
+ "columnName": "Routine_type"
+ }
+ },
+ "host": {
+ "type": "string",
+ "mysql": {
+ "columnName": "Host"
+ }
+ }
+ }
+}
diff --git a/loopback/locale/en.json b/loopback/locale/en.json
index 26650175d..949136459 100644
--- a/loopback/locale/en.json
+++ b/loopback/locale/en.json
@@ -196,6 +196,6 @@
"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"
+ "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets",
+ "Try again": "Try again"
}
-
diff --git a/loopback/locale/es.json b/loopback/locale/es.json
index b42720458..a4de6f997 100644
--- a/loopback/locale/es.json
+++ b/loopback/locale/es.json
@@ -224,7 +224,7 @@
"date in the future": "Fecha en el futuro",
"reference duplicated": "Referencia duplicada",
"This ticket is already a refund": "Este ticket ya es un abono",
- "isWithoutNegatives": "isWithoutNegatives",
+ "isWithoutNegatives": "Sin negativos",
"routeFk": "routeFk",
"Can't change the password of another worker": "No se puede cambiar la contraseña de otro trabajador",
"No hay un contrato en vigor": "No hay un contrato en vigor",
@@ -323,7 +323,9 @@
"The response is not a PDF": "La respuesta no es un PDF",
"Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
- "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina",
- "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina",
- "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada"
+ "Incoterms data for consignee is missing": "Faltan los datos de los Incoterms para el consignatario",
+ "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada",
+ "User disabled": "Usuario desactivado",
+ "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"
}
diff --git a/loopback/server/model-config.json b/loopback/server/model-config.json
index 52b539f60..33ef3797d 100644
--- a/loopback/server/model-config.json
+++ b/loopback/server/model-config.json
@@ -49,5 +49,13 @@
},
"Container": {
"dataSource": "vn"
+ },
+ "ProcsPriv": {
+ "dataSource": "vn",
+ "options": {
+ "mysql": {
+ "table": "mysql.procs_priv"
+ }
+ }
}
-}
\ No newline at end of file
+}
diff --git a/modules/account/back/model-config.json b/modules/account/back/model-config.json
index a4eb9fa57..b4bd6dbaf 100644
--- a/modules/account/back/model-config.json
+++ b/modules/account/back/model-config.json
@@ -35,6 +35,9 @@
"SambaConfig": {
"dataSource": "vn"
},
+ "SignInLog": {
+ "dataSource": "vn"
+ },
"Sip": {
"dataSource": "vn"
},
diff --git a/modules/account/back/models/sign_in-log.json b/modules/account/back/models/sign_in-log.json
new file mode 100644
index 000000000..44575b013
--- /dev/null
+++ b/modules/account/back/models/sign_in-log.json
@@ -0,0 +1,34 @@
+{
+ "name": "SignInLog",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "account.signInLog"
+ }
+ },
+ "properties": {
+ "id": {
+ "id": true,
+ "type": "string"
+ },
+ "creationDate": {
+ "type": "date"
+ },
+ "userFk": {
+ "type": "number"
+ },
+ "ip": {
+ "type": "string"
+ }
+ },
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "VnUser",
+ "foreignKey": "userFk"
+ }
+ },
+ "scope": {
+ "order": ["creationDate DESC", "id DESC"]
+ }
+}
diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js
index 04f6df299..800a4ea83 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/specs/transferinvoice.spec.js
@@ -27,7 +27,7 @@ describe('InvoiceOut tranferInvoice()', () => {
ref: 'T4444444',
newClientFk: 1,
cplusRectificationId: 1,
- cplusInvoiceType477Id: 1,
+ siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1
};
ctx.args = args;
@@ -52,7 +52,7 @@ describe('InvoiceOut tranferInvoice()', () => {
ref: 'T1111111',
newClientFk: 1101,
cplusRectificationId: 1,
- cplusInvoiceType477Id: 1,
+ siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1
};
ctx.args = args;
diff --git a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js
index 8a0609b8d..dde535c99 100644
--- a/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js
+++ b/modules/invoiceOut/back/methods/invoiceOut/transferInvoice.js
@@ -27,7 +27,7 @@ module.exports = Self => {
required: true
},
{
- arg: 'cplusInvoiceType477Id',
+ arg: 'siiTypeInvoiceOutId',
type: 'number',
required: true
},
@@ -93,7 +93,7 @@ module.exports = Self => {
correctingFk: invoiceId,
correctedFk: args.id,
cplusRectificationTypeFk: args.cplusRectificationId,
- cplusInvoiceType477Fk: args.cplusInvoiceType477Id,
+ siiTypeInvoiceOutFk: args.siiTypeInvoiceOutId,
invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId
}, myOptions);
diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json
index 23246893b..9c7512429 100644
--- a/modules/invoiceOut/back/model-config.json
+++ b/modules/invoiceOut/back/model-config.json
@@ -41,7 +41,7 @@
"InvoiceCorrection": {
"dataSource": "vn"
},
- "CplusInvoiceType477": {
+ "SiiTypeInvoiceOut": {
"dataSource": "vn"
}
}
diff --git a/modules/invoiceOut/back/models/invoice-correction.json b/modules/invoiceOut/back/models/invoice-correction.json
index 48bd172a6..43e4f07ef 100644
--- a/modules/invoiceOut/back/models/invoice-correction.json
+++ b/modules/invoiceOut/back/models/invoice-correction.json
@@ -18,11 +18,11 @@
"cplusRectificationTypeFk": {
"type": "number"
},
- "cplusInvoiceType477Fk": {
+ "siiTypeInvoiceOutFk": {
"type": "number"
},
"invoiceCorrectionTypeFk": {
"type": "number"
}
}
-}
\ No newline at end of file
+}
diff --git a/modules/invoiceOut/back/models/cplus-invoice-type-477.json b/modules/invoiceOut/back/models/sii-type-invoice-out.json
similarity index 78%
rename from modules/invoiceOut/back/models/cplus-invoice-type-477.json
rename to modules/invoiceOut/back/models/sii-type-invoice-out.json
index 840a9a7e4..17b312617 100644
--- a/modules/invoiceOut/back/models/cplus-invoice-type-477.json
+++ b/modules/invoiceOut/back/models/sii-type-invoice-out.json
@@ -1,9 +1,9 @@
{
- "name": "CplusInvoiceType477",
+ "name": "SiiTypeInvoiceOut",
"base": "VnModel",
"options": {
"mysql": {
- "table": "cplusInvoiceType477"
+ "table": "siiTypeInvoiceOut"
}
},
"properties": {
@@ -16,4 +16,4 @@
"type": "string"
}
}
-}
\ No newline at end of file
+}
diff --git a/modules/invoiceOut/front/descriptor-menu/index.html b/modules/invoiceOut/front/descriptor-menu/index.html
index 7d465f4ea..0052f0c03 100644
--- a/modules/invoiceOut/front/descriptor-menu/index.html
+++ b/modules/invoiceOut/front/descriptor-menu/index.html
@@ -6,8 +6,8 @@
+ url="SiiTypeInvoiceOuts"
+ data="siiTypeInvoiceOut">
Transfer invoice to...
@@ -223,15 +223,15 @@
-
-
\ No newline at end of file
+
diff --git a/modules/invoiceOut/front/descriptor-menu/index.js b/modules/invoiceOut/front/descriptor-menu/index.js
index 7d2644158..d3862a753 100644
--- a/modules/invoiceOut/front/descriptor-menu/index.js
+++ b/modules/invoiceOut/front/descriptor-menu/index.js
@@ -132,7 +132,7 @@ class Controller extends Section {
ref: this.invoiceOut.ref,
newClientFk: this.invoiceOut.client.id,
cplusRectificationId: this.cplusRectificationType,
- cplusInvoiceType477Id: this.cplusInvoiceType477,
+ siiTypeInvoiceOutId: this.siiTypeInvoiceOut,
invoiceCorrectionTypeId: this.invoiceCorrectionType
};
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {
diff --git a/modules/invoiceOut/front/negative-bases/index.js b/modules/invoiceOut/front/negative-bases/index.js
index f60668b20..7ce610513 100644
--- a/modules/invoiceOut/front/negative-bases/index.js
+++ b/modules/invoiceOut/front/negative-bases/index.js
@@ -7,7 +7,7 @@ export default class Controller extends Section {
super($element, $);
this.vnReport = vnReport;
- const now = new Date();
+ const now = Date.vnNew();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
this.params = {
diff --git a/modules/item/back/methods/item/setVisibleDiscard.js b/modules/item/back/methods/item/setVisibleDiscard.js
index 08cc507c3..16cef6baf 100644
--- a/modules/item/back/methods/item/setVisibleDiscard.js
+++ b/modules/item/back/methods/item/setVisibleDiscard.js
@@ -20,8 +20,7 @@ module.exports = Self => {
},
{
arg: 'addressFk',
- type: 'Number',
- required: true,
+ type: 'Any',
description: 'The address id'
}],
http: {
diff --git a/modules/item/back/models/item-shelving.json b/modules/item/back/models/item-shelving.json
index 61d05539e..bb1a141c4 100644
--- a/modules/item/back/models/item-shelving.json
+++ b/modules/item/back/models/item-shelving.json
@@ -1,6 +1,6 @@
{
"name": "ItemShelving",
- "base": "VnModel",
+ "base": "Loggable",
"options": {
"mysql": {
"table": "itemShelving"
diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js
index 1a497da24..610826283 100644
--- a/modules/ticket/back/methods/sale/updateQuantity.js
+++ b/modules/ticket/back/methods/sale/updateQuantity.js
@@ -64,7 +64,10 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions);
const oldQuantity = sale.quantity;
- const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
+ const result = await sale.updateAttributes({
+ quantity: newQuantity,
+ originalQuantity: newQuantity
+ }, myOptions);
const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) {
diff --git a/modules/ticket/back/methods/ticket/closeAll.js b/modules/ticket/back/methods/ticket/closeAll.js
index 660c16893..46c45aa92 100644
--- a/modules/ticket/back/methods/ticket/closeAll.js
+++ b/modules/ticket/back/methods/ticket/closeAll.js
@@ -32,31 +32,30 @@ module.exports = Self => {
throw new UserError('You cannot close tickets for today');
const tickets = await Self.rawSql(`
- SELECT
- t.id,
- t.clientFk,
- t.companyFk,
- c.name clientName,
- c.email recipient,
- c.salesPersonFk,
- c.isToBeMailed,
- c.hasToInvoice,
- co.hasDailyInvoice,
- eu.email salesPersonEmail
- FROM ticket t
- JOIN agencyMode am ON am.id = t.agencyModeFk
- JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
- JOIN ticketState ts ON ts.ticketFk = t.id
- JOIN alertLevel al ON al.id = ts.alertLevel
- JOIN client c ON c.id = t.clientFk
- JOIN province p ON p.id = c.provinceFk
- JOIN country co ON co.id = p.countryFk
- LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
- WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered'))
- AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
- AND util.dayEnd(?)
- AND t.refFk IS NULL
- GROUP BY t.id
+ SELECT t.id,
+ t.clientFk,
+ t.companyFk,
+ c.name clientName,
+ c.email recipient,
+ c.salesPersonFk,
+ c.isToBeMailed,
+ c.hasToInvoice,
+ co.hasDailyInvoice,
+ eu.email salesPersonEmail,
+ t.addressFk
+ FROM ticket t
+ JOIN agencyMode am ON am.id = t.agencyModeFk
+ JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
+ JOIN ticketState ts ON ts.ticketFk = t.id
+ JOIN alertLevel al ON al.id = ts.alertLevel
+ JOIN client c ON c.id = t.clientFk
+ JOIN province p ON p.id = c.provinceFk
+ JOIN country co ON co.id = p.countryFk
+ LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
+ WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered'))
+ AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY) AND util.dayEnd(?)
+ AND t.refFk IS NULL
+ GROUP BY t.id
`, [toDate, toDate]);
await closure(ctx, Self, tickets);
diff --git a/modules/ticket/back/methods/ticket/closure.js b/modules/ticket/back/methods/ticket/closure.js
index 9f9aec9bd..1d04679d3 100644
--- a/modules/ticket/back/methods/ticket/closure.js
+++ b/modules/ticket/back/methods/ticket/closure.js
@@ -5,177 +5,178 @@ const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage');
module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
- const userId = ctx.req.accessToken.userId;
- if (tickets.length == 0) return;
+ const userId = ctx.req.accessToken.userId;
+ if (tickets.length == 0) return;
- const failedtickets = [];
- for (const ticket of tickets) {
- try {
- await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
+ const failedtickets = [];
+ for (const ticket of tickets) {
+ try {
+ await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
- const [invoiceOut] = await Self.rawSql(`
- SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
- FROM ticket t
- JOIN invoiceOut io ON io.ref = t.refFk
- JOIN company cny ON cny.id = io.companyFk
- WHERE t.id = ?
- `, [ticket.id]);
+ const [invoiceOut] = await Self.rawSql(`
+ SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
+ FROM ticket t
+ JOIN invoiceOut io ON io.ref = t.refFk
+ JOIN company cny ON cny.id = io.companyFk
+ WHERE t.id = ?
+ `, [ticket.id]);
- const mailOptions = {
- overrideAttachments: true,
- attachments: []
- };
+ const mailOptions = {
+ overrideAttachments: true,
+ attachments: []
+ };
- const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
+ const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
- if (invoiceOut) {
- const args = {
- reference: invoiceOut.ref,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ if (invoiceOut) {
+ const args = {
+ reference: invoiceOut.ref,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail
+ };
- const invoiceReport = new Report('invoice', args);
- const stream = await invoiceReport.toPdfStream();
+ const invoiceReport = new Report('invoice', args);
+ const stream = await invoiceReport.toPdfStream();
- const issued = invoiceOut.issued;
- const year = issued.getFullYear().toString();
- const month = (issued.getMonth() + 1).toString();
- const day = issued.getDate().toString();
+ const issued = invoiceOut.issued;
+ const year = issued.getFullYear().toString();
+ const month = (issued.getMonth() + 1).toString();
+ const day = issued.getDate().toString();
- const fileName = `${year}${invoiceOut.ref}.pdf`;
+ const fileName = `${year}${invoiceOut.ref}.pdf`;
- // Store invoice
- await storage.write(stream, {
- type: 'invoice',
- path: `${year}/${month}/${day}`,
- fileName: fileName
- });
+ // Store invoice
+ await storage.write(stream, {
+ type: 'invoice',
+ path: `${year}/${month}/${day}`,
+ fileName: fileName
+ });
- await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
+ await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
- if (isToBeMailed) {
- const invoiceAttachment = {
- filename: fileName,
- content: stream
- };
+ if (isToBeMailed) {
+ const invoiceAttachment = {
+ filename: fileName,
+ content: stream
+ };
- if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
- const exportation = new Report('exportation', args);
- const stream = await exportation.toPdfStream();
- const fileName = `CITES-${invoiceOut.ref}.pdf`;
+ if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
+ const exportation = new Report('exportation', args);
+ const stream = await exportation.toPdfStream();
+ const fileName = `CITES-${invoiceOut.ref}.pdf`;
- mailOptions.attachments.push({
- filename: fileName,
- content: stream
- });
- }
+ mailOptions.attachments.push({
+ filename: fileName,
+ content: stream
+ });
+ }
- mailOptions.attachments.push(invoiceAttachment);
+ mailOptions.attachments.push(invoiceAttachment);
- const email = new Email('invoice', args);
- await email.send(mailOptions);
- }
- } else if (isToBeMailed) {
- const args = {
- id: ticket.id,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ const email = new Email('invoice', args);
+ await email.send(mailOptions);
+ }
+ } else if (isToBeMailed) {
+ const args = {
+ id: ticket.id,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail
+ };
- const email = new Email('delivery-note-link', args);
- await email.send();
- }
+ const email = new Email('delivery-note-link', args);
+ await email.send();
+ }
- // Incoterms authorization
- const [{firstOrder}] = await Self.rawSql(`
- SELECT COUNT(*) as firstOrder
- FROM ticket t
- JOIN client c ON c.id = t.clientFk
- WHERE t.clientFk = ?
- AND NOT t.isDeleted
- AND c.isVies
- `, [ticket.clientFk]);
+ // Incoterms authorization
+ const [{firstOrder}] = await Self.rawSql(`
+ SELECT COUNT(*) as firstOrder
+ FROM ticket t
+ JOIN client c ON c.id = t.clientFk
+ WHERE t.clientFk = ?
+ AND NOT t.isDeleted
+ AND c.isVies
+ `, [ticket.clientFk]);
- if (firstOrder == 1) {
- const args = {
- id: ticket.clientFk,
- companyId: ticket.companyFk,
- recipientId: ticket.clientFk,
- recipient: ticket.recipient,
- replyTo: ticket.salesPersonEmail
- };
+ if (firstOrder == 1) {
+ const args = {
+ id: ticket.clientFk,
+ companyId: ticket.companyFk,
+ recipientId: ticket.clientFk,
+ recipient: ticket.recipient,
+ replyTo: ticket.salesPersonEmail,
+ addressId: ticket.addressFk
+ };
- const email = new Email('incoterms-authorization', args);
- await email.send();
+ const email = new Email('incoterms-authorization', args);
+ await email.send();
- const [sample] = await Self.rawSql(
- `SELECT id
- FROM sample
- WHERE code = 'incoterms-authorization'
- `);
+ const [sample] = await Self.rawSql(
+ `SELECT id
+ FROM sample
+ WHERE code = 'incoterms-authorization'
+ `);
- await Self.rawSql(`
- INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
- `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
- };
- } catch (error) {
- // Domain not found
- if (error.responseCode == 450)
- return invalidEmail(ticket);
+ await Self.rawSql(`
+ INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
+ `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
+ }
+ } catch (error) {
+ // Domain not found
+ if (error.responseCode == 450)
+ return invalidEmail(ticket);
- // Save tickets on a list of failed ids
- failedtickets.push({
- id: ticket.id,
- stacktrace: error
- });
- }
- }
+ // Save tickets on a list of failed ids
+ failedtickets.push({
+ id: ticket.id,
+ stacktrace: error
+ });
+ }
+ }
- // Send email with failed tickets
- if (failedtickets.length > 0) {
- let body = 'This following tickets have failed:
';
+ // Send email with failed tickets
+ if (failedtickets.length > 0) {
+ let body = 'This following tickets have failed:
';
- for (const ticket of failedtickets) {
- body += `Ticket: ${ticket.id}
- ${ticket.stacktrace}
`;
- }
+ for (const ticket of failedtickets) {
+ body += `Ticket: ${ticket.id}
+ ${ticket.stacktrace}
`;
+ }
- smtp.send({
- to: config.app.reportEmail,
- subject: '[API] Nightly ticket closure report',
- html: body
- });
- }
+ smtp.send({
+ to: config.app.reportEmail,
+ subject: '[API] Nightly ticket closure report',
+ html: body
+ });
+ }
- async function invalidEmail(ticket) {
- await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
- ticket.clientFk
- ], {userId});
+ async function invalidEmail(ticket) {
+ await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
+ ticket.clientFk
+ ], {userId});
- const oldInstance = `{"email": "${ticket.recipient}"}`;
- const newInstance = `{"email": ""}`;
- await Self.rawSql(`
- INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
- VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
- ticket.clientFk,
- oldInstance,
- newInstance
- ], {userId});
+ const oldInstance = `{"email": "${ticket.recipient}"}`;
+ const newInstance = `{"email": ""}`;
+ await Self.rawSql(`
+ INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
+ VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
+ ticket.clientFk,
+ oldInstance,
+ newInstance
+ ], {userId});
- const body = `No se ha podido enviar el albarán ${ticket.id}
- al cliente ${ticket.clientFk} - ${ticket.clientName}
- porque la dirección de email "${ticket.recipient}" no es correcta
- o no está disponible.
- Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
- Actualiza la dirección de email con una correcta.`;
+ const body = `No se ha podido enviar el albarán ${ticket.id}
+ al cliente ${ticket.clientFk} - ${ticket.clientName}
+ porque la dirección de email "${ticket.recipient}" no es correcta
+ o no está disponible.