WIP: 5739-dockerRefactor #1822

Draft
alexm wants to merge 67 commits from 5739-dockerRefactor into dev
48 changed files with 851 additions and 382 deletions
Showing only changes of commit 63bbc61944 - Show all commits

View File

@ -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

View File

@ -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/)

View File

@ -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 => {

View File

@ -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
};

View File

@ -74,7 +74,7 @@ BEGIN
clientFk,
dued,
companyFk,
cplusInvoiceType477Fk
siiTypeInvoiceOutFk
)
SELECT
1,

View File

@ -96,7 +96,7 @@ BEGIN
clientFk,
dued,
companyFk,
cplusInvoiceType477Fk
siiTypeInvoiceOutFk
)
SELECT
1,

View File

@ -96,7 +96,7 @@ BEGIN
clientFk,
dued,
companyFk,
cplusInvoiceType477Fk
siiTypeInvoiceOutFk
)
SELECT
1,

View File

@ -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');

View File

@ -0,0 +1 @@
CALL `account`.`role_sync`();

View File

@ -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
);

View File

@ -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 ;

View File

@ -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;
--

View File

@ -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);

View File

@ -26077,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`)
@ -29534,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.';
@ -30214,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,
@ -30224,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`),
@ -30392,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,
@ -58372,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,
@ -58952,7 +58952,7 @@ BEGIN
clientFk,
dued,
companyFk,
cplusInvoiceType477Fk
siiTypeInvoiceOutFk
)
SELECT
1,

View File

@ -46,7 +46,7 @@ TABLES=(
bookingPlanner
businessType
cplusInvoiceType472
cplusInvoiceType477
siiTypeInvoiceOut
cplusRectificationType
cplusSubjectOp
cplusTaxBreak

View File

@ -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"]',

View File

@ -21,7 +21,8 @@ module.exports = Self => {
const argString = params.map(() => '?').join(',');
const [response] = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
return response;
const response = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
if (!Array.isArray(response)) return;
return response[0];
};
};

View File

@ -1,7 +1,7 @@
module.exports = Self => {
Self.remoteMethodCtx('executeFunc', {
description: 'Return result of function',
accessType: '*',
accessType: 'EXECUTE',
accepts: [
{
arg: 'routine',

View File

@ -1,7 +1,7 @@
module.exports = Self => {
Self.remoteMethodCtx('executeProc', {
description: 'Return result of procedure',
accessType: '*',
accessType: 'EXECUTE',
accepts: [
{
arg: 'routine',

View File

@ -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"
}

View File

@ -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"
}

View File

@ -35,6 +35,9 @@
"SambaConfig": {
"dataSource": "vn"
},
"SignInLog": {
"dataSource": "vn"
},
"Sip": {
"dataSource": "vn"
},

View File

@ -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"]
}
}

View File

@ -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;

View File

@ -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);

View File

@ -41,7 +41,7 @@
"InvoiceCorrection": {
"dataSource": "vn"
},
"CplusInvoiceType477": {
"SiiTypeInvoiceOut": {
"dataSource": "vn"
}
}

View File

@ -18,11 +18,11 @@
"cplusRectificationTypeFk": {
"type": "number"
},
"cplusInvoiceType477Fk": {
"siiTypeInvoiceOutFk": {
"type": "number"
},
"invoiceCorrectionTypeFk": {
"type": "number"
}
}
}
}

View File

@ -1,9 +1,9 @@
{
"name": "CplusInvoiceType477",
"name": "SiiTypeInvoiceOut",
"base": "VnModel",
"options": {
"mysql": {
"table": "cplusInvoiceType477"
"table": "siiTypeInvoiceOut"
}
},
"properties": {
@ -16,4 +16,4 @@
"type": "string"
}
}
}
}

View File

@ -6,8 +6,8 @@
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="CplusInvoiceType477s"
data="cplusInvoiceType477">
url="SiiTypeInvoiceOuts"
data="siiTypeInvoiceOut">
</vn-crud-model>
<vn-crud-model
auto-load="true"
@ -24,7 +24,7 @@
<vn-item
vn-acl="administrative"
vn-acl-action="remove"
ng-click="transferInvoice.show()"
ng-click="transferInvoice.show()"
translate>
Transfer invoice to...
</vn-item>
@ -223,15 +223,15 @@
<vn-autocomplete
vn-one
vn-id="cplusInvoiceType"
data="cplusInvoiceType477"
data="siiTypeInvoiceOut"
show-field="description"
value-field="id"
required="true"
ng-model="$ctrl.cplusInvoiceType477"
ng-model="$ctrl.siiTypeInvoiceOut"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Class">
</vn-autocomplete>
<vn-autocomplete
<vn-autocomplete
vn-one
vn-id="invoiceCorrectionType"
data="invoiceCorrectionTypes"
@ -247,4 +247,4 @@
<tpl-buttons>
<button response="accept" translate>Transfer client</button>
</tpl-buttons>
</vn-dialog>
</vn-dialog>

View File

@ -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 => {

View File

@ -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 = {

View File

@ -20,8 +20,7 @@ module.exports = Self => {
},
{
arg: 'addressFk',
type: 'Number',
required: true,
type: 'Any',
description: 'The address id'
}],
http: {

View File

@ -1,6 +1,6 @@
{
"name": "ItemShelving",
"base": "VnModel",
"base": "Loggable",
"options": {
"mysql": {
"table": "itemShelving"

View File

@ -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) {

View File

@ -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);

View File

@ -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:<br/><br/>';
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>';
for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
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 <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta
o no está disponible.<br/><br/>
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 <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta
o no está disponible.<br/><br/>
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.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};

View File

@ -75,8 +75,7 @@ module.exports = Self => {
{
arg: 'option',
type: 'number',
description: 'Action id',
required: true
description: 'Action id'
},
{
arg: 'isWithoutNegatives',
@ -88,7 +87,18 @@ module.exports = Self => {
arg: 'withWarningAccept',
type: 'boolean',
description: 'Has pressed in confirm message',
}],
},
{
arg: 'newTicket',
type: 'number',
description: 'Ticket id you want to move the lines to, if applicable',
},
{
arg: 'keepPrice',
type: 'boolean',
description: 'If prices should be maintained',
}
],
returns: {
type: ['object'],
root: true
@ -141,12 +151,18 @@ module.exports = Self => {
const salesNewTicketLength = salesNewTicket.length;
if (salesNewTicketLength && sales.length != salesNewTicketLength) {
const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions);
const newTicket = await models.Ticket.transferSales(
ctx,
args.id,
args.newTicket,
salesNewTicket,
myOptions
);
args.id = newTicket.id;
}
}
const originalTicket = await models.Ticket.findOne({
const ticketToChange = await models.Ticket.findOne({
where: {id: args.id},
fields: [
'id',
@ -179,33 +195,38 @@ module.exports = Self => {
},
]
}, myOptions);
const originalTicket = JSON.parse(JSON.stringify(ticketToChange));
const ticketChanges = {
clientFk: args.clientFk,
nickname: args.nickname,
agencyModeFk: args.agencyModeFk,
addressFk: args.addressFk,
zoneFk: args.zoneFk,
warehouseFk: args.warehouseFk,
companyFk: args.companyFk,
shipped: args.shipped,
landed: args.landed,
isDeleted: args.isDeleted
};
args.routeFk = null;
let response;
if (args.keepPrice) {
ticketChanges.routeFk = null;
response = await ticketToChange.updateAttributes(ticketChanges, myOptions);
} else {
const hasToBeUnrouted = true;
response = await Self.rawSql(
'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[args.id].concat(Object.values(ticketChanges), [hasToBeUnrouted, args.option]),
myOptions
);
}
if (args.isWithoutNegatives === false) delete args.isWithoutNegatives;
const updatedTicket = Object.assign({}, args);
delete updatedTicket.ctx;
delete updatedTicket.option;
// Force to unroute ticket
const hasToBeUnrouted = true;
const query = 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const res = await Self.rawSql(query, [
args.id,
args.clientFk,
args.nickname,
args.agencyModeFk,
args.addressFk,
args.zoneFk,
args.warehouseFk,
args.companyFk,
args.shipped,
args.landed,
args.isDeleted,
hasToBeUnrouted,
args.option
], myOptions);
if (originalTicket.addressFk != updatedTicket.addressFk && args.id) {
if (ticketToChange.addressFk != updatedTicket.addressFk && args.id) {
await models.TicketObservation.destroyAll({
ticketFk: args.id
}, myOptions);
@ -232,10 +253,15 @@ module.exports = Self => {
const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0;
if (hasChanges) {
delete changes.old.client;
delete changes.old.address;
delete changes.new.client;
delete changes.new.address;
const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new);
const salesPersonId = originalTicket.client().salesPersonFk;
const salesPersonId = ticketToChange.client().salesPersonFk;
if (salesPersonId) {
const url = await Self.app.models.Url.getUrl();
@ -256,10 +282,10 @@ module.exports = Self => {
}
}
res.id = args.id;
response.id = args.id;
if (tx) await tx.commit();
return res;
return response;
} catch (e) {
if (tx) await tx.rollback();
throw e;

View File

@ -49,7 +49,6 @@ module.exports = function(Self) {
myOptions.transaction = tx;
}
let serial;
try {
const ticketToInvoice = await Self.rawSql(`
SELECT id
@ -60,7 +59,7 @@ module.exports = function(Self) {
where: {
id: {inq: ticketsIds}
},
fields: ['id', 'clientFk']
fields: ['id', 'clientFk', 'addressFk']
}, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions);
@ -72,13 +71,19 @@ module.exports = function(Self) {
throw new UserError(`This client can't be invoiced`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
const [result] = await Self.rawSql(query, [
const [{serial}] = await Self.rawSql(query, [
clientId,
companyFk,
invoiceType,
], myOptions);
serial = result.serial;
const invoiceOutSerial = await models.InvoiceOutSerial.findById(serial);
if (invoiceOutSerial?.taxAreaFk == 'WORLD') {
const address = await models.Address.findById(firstTicket.addressFk);
if (!address || !address.customsAgentFk || !address.incotermsFk)
throw new UserError('Incoterms data for consignee is missing');
}
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);

View File

@ -209,4 +209,112 @@ describe('ticket componentUpdate()', () => {
throw e;
}
});
describe('componentUpdate() keepPrice', () => {
it('should change shipped and keep price', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const saleId = 27;
const originDate = today;
const newDate = tomorrow;
const ticketID = 14;
newDate.setHours(0, 0, 0, 0, 0);
originDate.setHours(0, 0, 0, 0, 0);
const args = {
id: ticketID,
clientFk: 1104,
agencyModeFk: 2,
addressFk: 4,
zoneFk: 9,
warehouseFk: 1,
companyFk: 442,
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
isWithoutNegatives: false,
keepPrice: true
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const beforeSale = await models.Sale.findById(saleId, null, options);
await models.Ticket.componentUpdate(ctx, options);
const afterSale = await models.Sale.findById(saleId, null, options);
expect(beforeSale.price).toEqual(afterSale.price);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should change shipped and not keep price', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const saleId = 27;
const originDate = today;
const newDate = tomorrow;
const ticketID = 14;
newDate.setHours(0, 0, 0, 0, 0);
originDate.setHours(0, 0, 0, 0, 0);
const args = {
id: ticketID,
clientFk: 1104,
agencyModeFk: 2,
addressFk: 4,
zoneFk: 9,
warehouseFk: 1,
companyFk: 442,
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
isWithoutNegatives: false,
keepPrice: false
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const beforeSale = await models.Sale.findById(saleId, null, options);
await models.Ticket.componentUpdate(ctx, options);
const afterSale = await models.Sale.findById(saleId, null, options);
expect(beforeSale.price).not.toEqual(afterSale.price);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

@ -52,4 +52,31 @@ describe('ticket makeInvoice()', () => {
throw e;
}
});
it('should throw an error when invoicing a client without incoterms', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticketsId = [18];
await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketsId], options);
await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Incoterms data for consignee is missing`);
});
});

View File

@ -21,10 +21,17 @@
expr-builder="$ctrl.exprBuilder(param, value)"
>
<slot-actions>
<vn-button disabled="$ctrl.checked.length === 0"
<vn-button
disabled="$ctrl.checked.length === 0"
icon="keyboard_double_arrow_left"
ng-click="moveTicketsAdvance.show($event)"
vn-tooltip="Advance tickets">
vn-tooltip="Advance tickets with negatives">
</vn-button>
<vn-button
disabled="$ctrl.checked.length === 0"
icon="alt_route"
ng-click="splitTickets.show($event)"
vn-tooltip="Advance tickets without negatives">
</vn-button>
</slot-actions>
<slot-table>
@ -32,8 +39,14 @@
<thead>
<tr second-header>
<td></td>
<th colspan="7" translate>Destination</th>
<th colspan="9" translate>Origin</th>
<th colspan="7">
<span translate>Destination</span>
{{model.userParams.dateToAdvance| date: 'dd/MM/yyyy'}}
</th>
<th colspan="11">
<span translate>Origin</span>
{{model.userParams.dateFuture | date: 'dd/MM/yyyy'}}
</th>
</tr>
<tr>
<th shrink>
@ -48,9 +61,6 @@
<th field="id">
<span translate>ID</span>
</th>
<th field="shipped">
<span translate>Date</span>
</th>
<th field="ipt" title="{{'Item Packing Type' | translate}}">
<span>IPT</span>
</th>
@ -69,9 +79,6 @@
<th separator field="futureId">
<span translate>ID</span>
</th>
<th field="futureShipped">
<span translate>Date</span>
</th>
<th field="futureIpt" title="{{'Item Packing Type' | translate}}">
<span>IPT</span>
</th>
@ -105,7 +112,7 @@
</td>
<td>
<vn-icon
ng-show="ticket.futureAgency !== ticket.agency"
ng-show="ticket.futureAgency !== ticket.agency && ticket.agency"
icon="icon-agency-term"
title="{{$ctrl.agencies(ticket.futureAgency, ticket.agency)}}">
</vn-icon>
@ -114,12 +121,7 @@
<span
ng-click="ticketDescriptor.show($event, ticket.id)"
class="link">
{{::ticket.id | dashIfEmpty}}
</span>
</td>
<td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.shipped)}}">
{{::ticket.shipped | date: 'dd/MM/yyyy'}}
{{::ticket.id}}
</span>
</td>
<td>{{::ticket.ipt | dashIfEmpty}}</td>
@ -135,7 +137,7 @@
<span
class="chip {{$ctrl.totalPriceColor(ticket.totalWithVat)}}"
title="{{$ctrl.totalPriceTitle(ticket.totalWithVat) | translate}}">
{{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
{{::ticket.totalWithVat | currency: 'EUR': 2}}
</span>
</td>
<td separator>
@ -145,11 +147,6 @@
{{::ticket.futureId | dashIfEmpty}}
</span>
</td>
<td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.futureShipped)}}">
{{::ticket.futureShipped | date: 'dd/MM/yyyy'}}
</span>
</td>
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
@ -181,6 +178,38 @@
question="{{$ctrl.confirmationMessage}}"
message="Advance tickets">
</vn-confirm>
<vn-confirm
vn-id="splitTickets"
on-accept="$ctrl.splitTickets()"
question="{{$ctrl.confirmationMessage}}"
message="Advance tickets (without negatives)">
</vn-confirm>
<!-- Split progress -->
<vn-dialog
vn-id="splitProgress"
message="Progress">
<tpl-body class="split-progress">
<vn-vertical>
<vn-spinner
enable="splitProgress.enable">
</vn-spinner>
<vn-label-value
label="Total progress"
value="[{{$ctrl.progress.length}}/{{::$ctrl.checked.length}}]">
</vn-label-value>
<span class="vn-mt-md" ng-if="$ctrl.splitErrors.length" translate>Error list</span>
<vn-vertical ng-repeat="error in $ctrl.splitErrors">
<vn-label-value
label="{{error.id}}"
value="{{error.reason}}">
</vn-label-value>
</vn-vertical>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}" ng-click="splitProgress.cancel = true"/>
</tpl-buttons>
</vn-dialog>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>

View File

@ -75,20 +75,6 @@ export default class Controller extends Section {
});
}
compareDate(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0)
return 'warning';
if (comparation < 0)
return 'success';
}
get checked() {
const tickets = this.$.model.data || [];
const checkedLines = [];
@ -134,9 +120,16 @@ export default class Controller extends Section {
'\n' + this.$t(`Destination agency`, {agency: agency});
}
moveTicketsAdvance() {
async moveTicketsAdvance() {
let ticketsToMove = [];
this.checked.forEach(ticket => {
for (const ticket of this.checked) {
if (!ticket.id) {
try {
const {query, params} = await this.requestComponentUpdate(ticket, false);
this.$http.post(query, params);
} catch (e) {}
continue;
}
ticketsToMove.push({
originId: ticket.futureId,
destinationId: ticket.id,
@ -144,15 +137,105 @@ export default class Controller extends Section {
destinationShipped: ticket.shipped,
workerFk: ticket.workerFk
});
});
}
const params = {tickets: ticketsToMove};
return this.$http.post('Tickets/merge', params)
.then(() => {
this.$.model.refresh();
this.vnApp.showSuccess(this.$t('Success'));
this.refresh();
if (ticketsToMove.length)
this.vnApp.showSuccess(this.$t('Success', {tickets: ticketsToMove.length}));
});
}
async getLanded(params) {
const query = `Agencies/getLanded`;
return this.$http.get(query, {params}).then(res => {
if (res.data)
return res.data;
return this.vnApp.showError(
this.$t(`No delivery zone available for this landing date`)
);
});
}
async splitTickets() {
this.progress = [];
this.splitErrors = [];
this.$.splitTickets.hide();
this.$.splitProgress.enable = true;
this.$.splitProgress.cancel = false;
this.$.splitProgress.show();
for (const ticket of this.checked) {
if (this.$.splitProgress.cancel) break;
try {
const {query, params} = await this.requestComponentUpdate(ticket, true);
this.$http.post(query, params)
.catch(e => {
this.splitErrors.push({id: ticket.futureId, reason: e.message});
})
.finally(() => this.progressAdd(ticket.futureId));
} catch (e) {
this.splitErrors.push({id: ticket.futureId, reason: e.message});
this.progressAdd(ticket.futureId);
}
}
}
progressAdd(ticketId) {
this.progress.push(ticketId);
if (this.progress.length == this.checked.length) {
this.$.splitProgress.enable = false;
this.refresh();
if ((this.progress.length - this.splitErrors.length) > 0) {
this.vnApp.showSuccess(this.$t('Success', {
tickets: this.progress.length - this.splitErrors.length
}));
}
}
}
async requestComponentUpdate(ticket, isWithoutNegatives) {
const query = `tickets/${ticket.futureId}/componentUpdate`;
if (!ticket.landed) {
const newLanded = await this.getLanded({
shipped: this.$.model.userParams.dateToAdvance,
addressFk: ticket.addressFk,
agencyModeFk: ticket.agencyModeFk,
warehouseFk: ticket.warehouseFk
});
if (!newLanded)
throw new Error(this.$t(`No delivery zone available for this landing date`));
ticket.landed = newLanded.landed;
ticket.zoneFk = newLanded.zoneFk;
}
const params = {
clientFk: ticket.clientFk,
nickname: ticket.nickname,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
addressFk: ticket.addressFk,
zoneFk: ticket.zoneFk ?? ticket.futureZoneFk,
warehouseFk: ticket.warehouseFk,
companyFk: ticket.companyFk,
shipped: this.$.model.userParams.dateToAdvance,
landed: ticket.landed,
isDeleted: false,
isWithoutNegatives,
newTicket: ticket.id ?? undefined,
keepPrice: true
};
return {query, params};
}
refresh() {
this.$.model.refresh();
this.$checkAll = null;
}
exprBuilder(param, value) {
switch (param) {
case 'id':

View File

@ -24,31 +24,6 @@ describe('Component vnTicketAdvance', () => {
}];
}));
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let today = Date.vnNew();
let result = controller.compareDate(today);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = Date.vnNew();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = Date.vnNew();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('checked()', () => {
it('should return an array of checked tickets', () => {
const result = controller.checked;

View File

@ -1,7 +1,9 @@
Advance tickets: Adelantar tickets
Advance tickets with negatives: Adelantar tickets con negativos
Advance tickets without negatives: Adelantar tickets sin negativos
Search advance tickets by date: Busca tickets para adelantar por fecha
Advance confirmation: ¿Desea adelantar {{checked}} tickets?
Success: Tickets movidos correctamente
Success: "{{tickets}} Tickets movidos correctamente"
Lines: Líneas
Liters: Litros
Item Packing Type: Encajado
@ -9,3 +11,7 @@ Origin agency: "Agencia origen: {{agency}}"
Destination agency: "Agencia destino: {{agency}}"
Less than 50€: Menor a 50€
Not Movable: No movibles
Error list: Lista de errores
Progress: Progreso
Total progress: Progreso total
Advance tickets (without negatives): Adelantar tickets (sin negativos)

View File

@ -5,3 +5,12 @@ vn-ticket-advance{
color: #f7931e
}
}
.split-progress {
width: 40em;
}
@media screen and (max-width: 600px) {
.split-progress {
width: 100%;
}
}

View File

@ -114,7 +114,8 @@ class Controller extends Component {
isDeleted: this.ticket.isDeleted,
option: parseInt(this.ticket.option),
isWithoutNegatives: this.ticket.withoutNegatives,
withWarningAccept: this.ticket.withWarningAccept
withWarningAccept: this.ticket.withWarningAccept,
keepPrice: false
};
this.$http.post(query, params)

View File

@ -38,7 +38,7 @@
</vn-label-value>
<vn-label-value
label="Extension"
>
>
<vn-link-phone
phone-number="$ctrl.worker.sip.extension"
></vn-link-phone>
@ -61,8 +61,6 @@
</div>
<div ng-transclude="btnTwo">
<vn-quick-link
vn-acl="hr"
vn-acl-action="remove"
tooltip="Go to user"
state="['account.card.summary', {id: $ctrl.id}]"
icon="face">
@ -75,13 +73,13 @@
<vn-popup vn-id="summary">
<vn-worker-summary worker="$ctrl.worker"></vn-worker-summary>
</vn-popup>
<vn-dialog
<vn-dialog
vn-id="setPassword"
on-accept="$ctrl.setPassword($ctrl.worker.password)"
message="Reset password"
>
<tpl-body>
<vn-textfield
<vn-textfield
vn-one
label="New password"
required="true"

View File

@ -1,7 +1,7 @@
SELECT io.issued,
c.socialName,
c.street postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
c.fi,
io.clientFk,
c.postcode,
c.city,
@ -50,7 +50,7 @@ SELECT io.issued,
) sub2 ON TRUE
JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE'
JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'WORLD'
JOIN vn.country cty ON cty.id = c.countryFk
JOIN vn.payMethod pm ON pm.id = c .payMethodFk
JOIN vn.company co ON co.id=io.companyFk