Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4764-serviceAbono
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Carlos Satorres 2023-06-09 14:23:55 +02:00
commit 9ca82a2f79
102 changed files with 1400 additions and 1085 deletions

View File

@ -1,6 +1,6 @@
extends: [eslint:recommended, google, plugin:jasmine/recommended]
parserOptions:
ecmaVersion: 2018
ecmaVersion: 2020
sourceType: "module"
plugins:
- jasmine
@ -35,4 +35,4 @@ rules:
space-in-parens: ["error", "never"]
jasmine/no-focused-tests: 0
jasmine/prefer-toHaveBeenCalledWith: 0
arrow-spacing: ["error", { "before": true, "after": true }]
arrow-spacing: ["error", { "before": true, "after": true }]

View File

@ -5,14 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2326.01] - 2023-06-29
### Added
### Changed
### Fixed
-
## [2324.01] - 2023-06-08
### Added
-
- (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
- (General -> Desplegables) Mejorada eficiencia de carga de datos
### Changed
-
- (General -> Permisos) Mejorada seguridad
### Fixed
-

View File

@ -11,9 +11,9 @@ RUN apt-get update \
ca-certificates \
gnupg2 \
graphicsmagick \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2
&& npm install -g npm@9.6.6
# Puppeteer

8
Jenkinsfile vendored
View File

@ -39,7 +39,7 @@ pipeline {
NODE_ENV = ""
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci'
}
@ -57,14 +57,14 @@ pipeline {
parallel {
stage('Frontend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
}
}
}
stage('Backend') {
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'npm run test:back:ci'
}
}
@ -80,7 +80,7 @@ pipeline {
CREDENTIALS = credentials('docker-registry')
}
steps {
nodejs('node-v14') {
nodejs('node-v20') {
sh 'gulp build'
}

View File

@ -0,0 +1,127 @@
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;
DROP TEMPORARY TABLE IF EXISTS tmp.stock;
CREATE 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 < vDateFuture
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;
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE 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,
origin.futureAgency,
dest.lines,
dest.liters,
origin.futureLines - origin.hasStock AS notMovableLines,
(origin.futureLines = origin.hasStock) AS isFullMovable,
origin.classColor futureClassColor,
dest.classColor
FROM (
SELECT
s.ticketFk,
t.workerFk,
t.shipped,
t.totalWithVat,
st.name futureState,
t.addressFk,
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,
st.classColor
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
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
JOIN (
SELECT
t.id ticketFk,
t.addressFk,
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
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 != 0;
DROP TEMPORARY TABLE tmp.stock;
END$$
DELIMITER ;

View File

@ -0,0 +1,74 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro
*
* @param vOriginDated Fecha en cuestión
* @param vFutureDated Fecha en el futuro a sondear
* @param vWarehouseFk Identificador de vn.warehouse
*/
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT sv.ticketFk id,
sub2.id futureId,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
st.name state,
sub2.iptd futureIpt,
sub2.state futureState,
t.clientFk,
t.warehouseFk,
ts.alertLevel,
t.shipped,
sub2.shipped futureShipped,
t.workerFk,
st.code stateCode,
sub2.code futureStateCode,
st.classColor,
sub2.classColor futureClassColor
FROM vn.saleVolume sv
JOIN vn.sale s ON s.id = sv.saleFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticket t ON t.id = sv.ticketFk
JOIN vn.address a ON a.id = t.addressFk
JOIN vn.province p ON p.id = a.provinceFk
JOIN vn.country c ON c.id = p.countryFk
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.alertLevel al ON al.id = ts.alertLevel
LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id
LEFT JOIN (
SELECT *
FROM (
SELECT
t.addressFk,
t.id,
t.shipped,
st.name state,
st.code,
st.classColor,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
WHERE t.shipped BETWEEN vFutureDated
AND util.dayend(vFutureDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) sub
GROUP BY sub.addressFk
) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id
WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated)
AND t.warehouseFk = vWarehouseFk
AND al.code = 'FREE'
AND tp.ticketFk IS NULL
GROUP BY sv.ticketFk
HAVING futureId;
END$$
DELIMITER ;

View File

@ -0,0 +1,28 @@
CREATE TABLE `vn`.`buyConfig` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`monthsAgo` int(11) NOT NULL DEFAULT 6 COMMENT 'Meses desde la última compra',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
CREATE TABLE `vn`.`travelConfig` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`warehouseInFk` smallint(6) unsigned NOT NULL DEFAULT 8 COMMENT 'Warehouse de origen',
`warehouseOutFk` smallint(6) unsigned NOT NULL DEFAULT 60 COMMENT 'Warehouse destino',
`agencyFk` int(11) NOT NULL DEFAULT 1378 COMMENT 'Agencia por defecto',
`companyFk` smallint(5) unsigned NOT NULL DEFAULT 442 COMMENT 'Compañía por defecto',
PRIMARY KEY (`id`),
KEY `travelConfig_FK` (`warehouseInFk`),
KEY `travelConfig_FK_1` (`warehouseOutFk`),
KEY `travelConfig_FK_2` (`agencyFk`),
KEY `travelConfig_FK_3` (`companyFk`),
CONSTRAINT `travelConfig_FK` FOREIGN KEY (`warehouseInFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_1` FOREIGN KEY (`warehouseOutFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_2` FOREIGN KEY (`agencyFk`) REFERENCES `agencyMode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_3` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Entry', 'addFromPackaging', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Entry', 'addFromBuy', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Supplier', 'getItemsPackaging', 'READ', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`sector` DROP COLUMN `printerFk`;
ALTER TABLE `vn`.`sector` ADD COLUMN `mainPrinterFk` tinyint(3) unsigned;
ALTER TABLE `vn`.`sector` ADD CONSTRAINT sector_FK_1 FOREIGN KEY (mainPrinterFk) REFERENCES vn.printer(id) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`ticket` MODIFY COLUMN warehouseFk smallint(6) unsigned DEFAULT NULL NULL;

View File

View File

@ -179,6 +179,8 @@ INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAd
(2, 'printer2', 'path2', 1, 1 , NULL),
(4, 'printer4', 'path4', 0, NULL, '10.1.10.4');
UPDATE `vn`.`sector` SET mainPrinterFk = 1 WHERE id = 1;
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`)
VALUES
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL),
@ -905,7 +907,7 @@ INSERT INTO `vn`.`itemFamily`(`code`, `description`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`,
`comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`, `packingShelve`, `weightByPiece`)
VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL, 'V', 0, 15,3),
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'EMB', 0, NULL, 'V', 0, 15,3),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H', 0, 10,2),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL, 0, 5,5),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL,NULL),
@ -2729,6 +2731,7 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
VALUES
(1, 'print-email', 'notification fixture one'),
(2, 'invoice-electronic', 'A electronic invoice has been generated'),
(3, 'not-main-printer-configured', 'A printer distinct than main has been configured'),
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated');
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
@ -2783,7 +2786,9 @@ INSERT INTO `vn`.`ticketLog` (`originFk`, userFk, `action`, changedModel, oldIns
(7, 18, 'update', 'Sale', '{"quantity":1}', '{"quantity":10}', 1, NULL),
(7, 18, 'update', 'Ticket', '{"quantity":1,"concept":"Chest ammo box"}', '{"quantity":10,"concept":"Chest ammo box"}', 1, NULL),
(7, 18, 'update', 'Sale', '{"price":3}', '{"price":5}', 1, NULL),
(7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 1x0.5m de '5' a '10'");
(7, 18, 'update', NULL, NULL, NULL, NULL, "Cambio cantidad Melee weapon heavy shield 1x0.5m de '5' a '10'"),
(16, 9, 'update', 'Sale', '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', '{"quantity":8,"concept":"Shield", "price": 10.5, "itemFk": 1}' , 5689, 'Shield');
INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedModel, changedModelId, changedModelValue, oldInstance, newInstance, description)
VALUES
@ -2797,7 +2802,6 @@ INSERT INTO `vn`.`ticketLog` (originFk, userFk, `action`, creationDate, changedM
(1, 18, 'select', '2000-12-27 03:40:30', 'Ticket', 45, NULL , NULL, NULL, NULL),
(1, 18, 'insert', '2000-04-10 09:40:15', 'Sale', 5689, 'Shield' , NULL, '{"quantity":10,"concept":"Shield", "price": 10.5, "itemFk": 1}', NULL),
(1, 18, 'insert', '1999-05-09 10:00:00', 'Ticket', 45, 'Super Man' , NULL, '{"id":45,"clientFk":8608,"warehouseFk":60,"shipped":"2023-05-16T22:00:00.000Z","nickname":"Super Man","addressFk":48637,"isSigned":true,"isLabeled":true,"isPrinted":true,"packages":0,"hour":0,"created":"2023-05-16T11:42:56.000Z","isBlocked":false,"hasPriority":false,"companyFk":442,"agencyModeFk":639,"landed":"2023-05-17T22:00:00.000Z","isBoxed":true,"isDeleted":true,"zoneFk":713,"zonePrice":13,"zoneBonus":0}', NULL);
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', '1,6', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
@ -2886,6 +2890,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
(2, 1, 50, 2),
(3, 1, 0, 3);
INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agencyFk`, `companyFk`)
VALUES
(1, 1, 1, 1, 442);
INSERT INTO `vn`.`buyConfig` (`id`, `monthsAgo`)
VALUES
(1, 6);

View File

@ -595,6 +595,8 @@ export default {
moreMenuUpdateDiscount: 'vn-item[name="discount"]',
moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]',
moreMenuRefund: 'vn-item[name="refund"]',
refundWithWarehouse: 'vn-item[name="refundWithWarehouse"]',
refundWithoutWarehouse: 'vn-item[name="refundWithoutWarehouse"]',
moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input',
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',

View File

@ -64,6 +64,6 @@ describe('SmartTable SearchBar integration', () => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '13');
await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '3');
});
});

View File

@ -88,7 +88,8 @@ describe('Item fixed prices path', () => {
it('should reload the section and check the created price has the expected ID', async() => {
await page.goto(`http://localhost:5000/#!/item/fixed-price`);
await page.autocompleteSearch($.warehouseFilter, 'Warehouse one');
await page.click($.chip);
const result = await page.waitToGetProperty($.fourthItemID, 'value');
expect(result).toContain('13');

View File

@ -220,14 +220,25 @@ describe('Ticket Edit sale path', () => {
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSearchResult('17');
await page.accessToSection('ticket.card.sale');
});
it('should select the third sale and create a refund', async() => {
it('should select the first sale and create a refund with warehouse', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
it('should select the first sale and create a refund without warehouse', async() => {
await page.accessToSearchResult('18');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithoutWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
@ -246,7 +257,6 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);
@ -316,7 +326,7 @@ describe('Ticket Edit sale path', () => {
it('should confirm the transfered quantity is the correct one', async() => {
const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText');
expect(result).toContain('10');
expect(result).toContain('20');
});
it('should go back to the original ticket sales section', async() => {

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642'
xdescribe('Ticket Future path', () => {
describe('Ticket Future path', () => {
let browser;
let page;
let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Future path', () => {
await browser.close();
});
it('should show errors snackbar because of the required data', async() => {
it('should search with required data, check three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit);
@ -43,69 +42,58 @@ xdescribe('Ticket Future path', () => {
message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toBeDefined();
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketFuture.ipt, 'H');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('ipt=H');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.ipt);
await page.autocompleteSearch(selectors.ticketFuture.futureIpt, 'H');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('futureIpt=H');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureIpt);
await page.autocompleteSearch(selectors.ticketFuture.state, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('state=FREE');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.state);
await page.autocompleteSearch(selectors.ticketFuture.futureState, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('futureState=FREE');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.futureState);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.globalItems.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
});
// it('should search with the required data', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toBeDefined();
// });
// it('should search with the origin IPT', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketFuture.ipt, 'H');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('ipt=H');
// });
// it('should search with the destination IPT', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.ipt);
// await page.autocompleteSearch(selectors.ticketFuture.futureIpt, 'H');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('futureIpt=H');
// });
// it('should search with the origin grouped state', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.futureIpt);
// await page.autocompleteSearch(selectors.ticketFuture.state, 'Free');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('state=FREE');
// });
// it('should search with the destination grouped state', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.state);
// await page.autocompleteSearch(selectors.ticketFuture.futureState, 'Free');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('futureState=FREE');
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.futureState);
// await page.waitToClick(selectors.ticketFuture.submit);
// });
// it('should check the three last tickets and move to the future', async() => {
// await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
// await page.waitToClick(selectors.ticketFuture.multiCheck);
// await page.waitToClick(selectors.ticketFuture.firstCheck);
// await page.waitToClick(selectors.ticketFuture.moveButton);
// await page.waitToClick(selectors.globalItems.acceptButton);
// const message = await page.waitForSnackbar();
// expect(message.text).toContain('Tickets moved successfully!');
// });
});

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642'
xdescribe('Ticket Advance path', () => {
describe('Ticket Advance path', () => {
let browser;
let page;
let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Advance path', () => {
await browser.close();
});
it('should show errors snackbar because of the required data', async() => {
it('should search with the required data, check the first ticket and move to the present', async() => {
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.warehouseFk);
@ -44,45 +43,37 @@ xdescribe('Ticket Advance path', () => {
message = await page.waitForSnackbar();
expect(message.text).toContain('dateFuture is a required argument');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toBeDefined();
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketAdvance.futureIpt, 'H');
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toContain('futureIpt=H');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.futureIpt);
await page.waitToClick(selectors.ticketAdvance.submit);
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketAdvance.ipt, 'H');
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toContain('ipt=H');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.ipt);
await page.waitToClick(selectors.ticketAdvance.submit);
await page.waitToClick(selectors.ticketAdvance.firstCheck);
await page.waitToClick(selectors.ticketAdvance.moveButton);
await page.waitToClick(selectors.ticketAdvance.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
});
// it('should search with the required data', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toBeDefined();
// });
// it('should search with the origin IPT', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketAdvance.futureIpt, 'H');
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toContain('futureIpt=H');
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketAdvance.futureIpt);
// await page.waitToClick(selectors.ticketAdvance.submit);
// });
// it('should search with the destination IPT', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketAdvance.ipt, 'H');
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toContain('ipt=H');
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketAdvance.ipt);
// await page.waitToClick(selectors.ticketAdvance.submit);
// });
// it('should check the first ticket and move to the present', async() => {
// await page.waitToClick(selectors.ticketAdvance.firstCheck);
// await page.waitToClick(selectors.ticketAdvance.moveButton);
// await page.waitToClick(selectors.ticketAdvance.acceptButton);
// const message = await page.waitForSnackbar();
// expect(message.text).toContain('Tickets moved successfully!');
// });
});

View File

@ -49,7 +49,11 @@ describe('Claim summary path', () => {
});
it(`should click on the first sale ID making the item descriptor visible`, async() => {
await page.waitToClick(selectors.claimSummary.firstSaleItemId);
const firstItem = selectors.claimSummary.firstSaleItemId;
await page.evaluate(selectors => {
document.querySelector(selectors).scrollIntoView();
}, firstItem);
await page.click(firstItem);
await page.waitImgLoad(selectors.claimSummary.firstSaleDescriptorImage);
const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover);

View File

@ -1,4 +1,11 @@
@import "./variables";
@import "./effects";
@mixin mobile {
@media screen and (max-width: $mobile-width) {
@content;
}
}
@mixin browser($browser) {
html[data-browser*="#{$browser}"] & {
@content;

View File

@ -1,5 +1,3 @@
@import "./util";
$font-size: 11pt;
$menu-width: 256px;
$topbar-height: 56px;

View File

@ -1,4 +1,4 @@
@import "variables";
@import "util";
@keyframes fadein {
from {
@ -16,7 +16,7 @@ vn-background {
background-color: black;
z-index: 14;
@media screen and (max-width: $mobile-width) {
@include mobile {
&.shown {
display: block;
opacity: .3;

View File

@ -1,4 +1,4 @@
@import "effects";
@import "util";
vn-layout {
& > vn-topbar {
@ -134,7 +134,7 @@ vn-layout {
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
& > vn-topbar {
& > .start > .logo {
display: none;

View File

@ -16,7 +16,7 @@
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-sm vn-px-sm">
class="vn-w-sm vn-px-sm vn-pb-xl">
<div class="change vn-mb-sm" ng-repeat="log in $ctrl.logs">
<div class="left">
<vn-avatar class="vn-mt-xs"
@ -33,17 +33,6 @@
</div>
<vn-card class="detail">
<div class="header vn-pa-sm">
<div
class="action-date text-secondary text-caption vn-mr-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
<vn-icon
class="action vn-mr-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<div class="action-model">
<span class="model-name"
ng-if="::$ctrl.showModelName && log.changedModel"
@ -52,13 +41,27 @@
{{::log.changedModelI18n}}
</span>
</div>
<div
class="action-date text-secondary text-caption vn-ml-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
<vn-icon
class="action vn-ml-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
</div>
</div>
<div
class="model vn-pb-sm vn-px-sm"
title="{{::log.changedModelValue}}"
ng-if="::log.changedModelId || log.changedModelValue">
<div class="model vn-pb-sm vn-px-sm"
ng-if="::$ctrl.showModelName">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<span class="model-value">{{::log.changedModelValue}}</span>
<vn-icon
icon="filter_alt"
translate-attr="{title: 'Show all record changes'}"
ng-click="$ctrl.filterByEntity(log)">
</vn-icon>
<span class="model-value" title="{{::log.changedModelValue}}">{{::log.changedModelValue}}</span>
</div>
<div class="changes vn-pa-sm"
ng-class="{expanded: log.expand}"
@ -75,16 +78,16 @@
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span>
<vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value>
<vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'">
<vn-json-value value="::prop.old"></vn-json-value>
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
</div>
@ -96,6 +99,13 @@
</div>
</div>
</vn-data-viewer>
<vn-float-button
ng-if="model.userFilter"
icon="filter_alt_off"
translate-attr="{title: 'Quit filter'}"
ng-click="$ctrl.resetFilter()"
fixed-bottom-right>
</vn-float-button>
<vn-side-menu side="right">
<form vn-vertical
ng-model-options="{updateOn: 'change blur'}"
@ -163,12 +173,17 @@
data="$ctrl.models"
class="changed-model">
</vn-autocomplete>
<!-- FIXME: Cannot use LIKE with JSON columns
<vn-textfield
label="Changes"
ng-model="filter.changes">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by changes"
pointer>
</vn-icon>
</append>
</vn-textfield>
-->
<vn-vertical>
<vn-check
label="Creates"
@ -195,18 +210,6 @@
label="To"
ng-model="filter.to">
</vn-date-picker>
<vn-button-bar vn-vertical>
<vn-button
label="Filter"
ng-click="$ctrl.applyFilter(filter)">
</vn-button>
<vn-button
label="Reset"
class="flat"
ng-click="$ctrl.resetFilter()"
ng-if="model.userFilter">
</vn-button>
</vn-button-bar>
</form>
</vn-side-menu>
<vn-worker-descriptor-popover vn-id="workerDescriptor">

View File

@ -64,29 +64,47 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
if (!value) return;
const empty = {};
const validations = window.validations;
const castJsonValue = this.castJsonValue;
for (const log of value) {
const oldValues = log.oldInstance || empty;
const newValues = log.newInstance || empty;
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = firstUpper(locale.name) || log.changedModel;
let props = Object.keys(oldValues).concat(Object.keys(newValues));
let props = Object.keys(olds).concat(Object.keys(vals));
props = [...new Set(props)];
log.props = [];
for (const prop of props) {
if (prop.endsWith('$')) continue;
log.props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: this.castJsonValue(oldValues[prop]),
new: this.castJsonValue(newValues[prop])
old: getVal(olds, prop),
val: getVal(vals, prop)
});
}
log.props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n));
}
function getVal(vals, prop) {
let val, id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
}
}
get models() {
@ -113,10 +131,6 @@ export default class Controller extends Section {
: value;
}
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
relativeDate(dateVal) {
if (dateVal == null) return '';
const date = new Date(dateVal);
@ -150,14 +164,15 @@ export default class Controller extends Section {
if (value == null || value == '') return null;
switch (prop) {
case 'search':
if (/^[0-9]+$/.test(value))
return {changedModelId: value};
const or = [];
if (/^\s*[0-9]+\s*$/.test(value))
return {changedModelId: value.trim()};
else
return {changedModelValue: {like: `%${value}%`}};
case 'changes':
return {or: [
{oldInstance: {like: `%${value}%`}},
{newInstance: {like: `%${value}%`}},
{oldJson: {like: `%${value}%`}},
{newJson: {like: `%${value}%`}},
{description: {like: `%${value}%`}}
]};
case 'who':
@ -206,6 +221,14 @@ export default class Controller extends Section {
return this.$.model.applyFilter(lbFilter);
}
filterByEntity(log) {
this.$.filter = {
who: 'all',
search: log.changedModelId,
changedModel: log.changedModel
};
}
searchUser(search) {
if (/^[0-9]+$/.test(search)) {
return {id: search};
@ -238,3 +261,12 @@ ngModule.vnComponent('vnLog', {
url: '@'
}
});
ngModule.component('vnLogValue', {
template:
'<vn-json-value value="::$ctrl.val.val"></vn-json-value>' +
'<span ng-if="::$ctrl.val.id" class="id-value"> #{{::$ctrl.val.id}}</span>',
bindings: {
val: '<?',
}
});

View File

@ -2,6 +2,9 @@ Date: Fecha
Concept: Concepto
Search: Buscar
Search by id or concept: Buscar por identificador o concepto
Search by changes: |
Buscar por cambios. Los atributos deben buscarse por su nombre interno,
para obtenerlo situar el cursor sobre el atributo.
Entity: Entidad
Action: Acción
Author: Autor
@ -13,9 +16,12 @@ Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
All: Todo
System: Sistema
Details: Detalles
note: nota
Changes: Cambios
today: hoy
yesterday: ayer
Show all record changes: Mostrar todos los cambios realizados en el registro
Quit filter: Quitar filtro

View File

@ -1,5 +1,4 @@
@import "variables";
@import "effects";
@import "util";
vn-log {
.change {
@ -77,7 +76,7 @@ vn-log {
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 1.4em;
font-size: 18px;
&.notice {
background-color: $color-notice-medium
@ -98,7 +97,22 @@ vn-log {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-height: 18px;
& > vn-icon {
@extend %clickable-light;
vertical-align: middle;
padding: 2px;
margin: -2px;
font-size: 18px;
color: $color-font-secondary;
float: right;
display: none;
@include mobile {
display: initial;
}
}
& > .model-value {
font-style: italic;
}
@ -107,6 +121,9 @@ vn-log {
font-size: .9rem;
}
}
&:hover > .model > vn-icon {
display: initial;
}
}
}
.changes {
@ -144,3 +161,7 @@ vn-log {
}
}
}
vn-log-value > .id-value {
font-size: .9rem;
color: $color-font-secondary;
}

View File

@ -20,8 +20,6 @@ class Controller {
name: config.languages[code] ? config.languages[code] : code
});
}
vnConfig.initialize();
}
set lang(value) {

View File

@ -10,6 +10,9 @@ function config($stateProvider, $urlRouterProvider) {
.state('layout', {
abstract: true,
template: '<vn-layout></vn-layout>',
resolve: {
config: ['vnConfig', vnConfig => vnConfig.initialize()]
}
})
.state('outLayout', {
abstract: true,

View File

@ -1,5 +1,4 @@
@import "./variables";
@import "./effects";
@import "./util";
form vn-horizontal {
align-items: center;
@ -22,10 +21,10 @@ form vn-horizontal {
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
flex-direction: column;
align-items: initial;
& > * {
&,
&:first-child,

View File

@ -1,4 +1,61 @@
{
"name": "Log",
"base": "VnModel"
"name": "Log",
"base": "VnModel",
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"oldJson": {
"type": "String",
"mysql": {"columnName": "oldInstance"}
},
"newJson": {
"type": "String",
"mysql": {"columnName": "newInstance"}
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -1,6 +1,8 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const UserError = require('vn-loopback/util/user-error');
const utils = require('loopback/lib/utils');
const {util} = require('webpack');
module.exports = function(Self) {
Self.ParameterizedSQL = ParameterizedSQL;
@ -164,23 +166,21 @@ module.exports = function(Self) {
function rewriteMethod(methodName) {
const realMethod = this[methodName];
return async(data, options, cb) => {
if (options instanceof Function) {
cb = options;
options = null;
}
return function(...args) {
let cb;
const lastArg = args[args.length - 1];
if (lastArg instanceof Function) {
cb = lastArg;
args.pop();
} else
cb = utils.createPromiseCallback();
try {
const result = await realMethod.call(this, data, options);
if (cb) cb(null, result);
else return result;
} catch (err) {
let myErr = replaceErr(err, replaceErrFunc);
if (cb) cb(myErr);
else
throw myErr;
}
args.push(function(err, res) {
if (err) err = replaceErr(err, replaceErrFunc);
cb(err, res);
});
realMethod.apply(this, args);
return cb.promise;
};
}

View File

@ -1,4 +1,5 @@
name: subrole
columns:
id: id
role: rol
inheritsFrom: inherits

View File

@ -1,4 +1,5 @@
name: subrol
columns:
id: id
role: rol
inheritsFrom: hereda

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "account.roleLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "account.userLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "claimLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: sender
sender: sender number
destination: destination
message: message
statusCode: status code
status: status
created: created

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: remitente
sender: número remitente
destination: destinatario
message: mensaje
statusCode: código estado
status: estado
created: creado

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "clientLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -68,7 +68,7 @@
</span>
</vn-td>
<vn-td number>{{::clientInforma.rating}}</vn-td>
<vn-td number>{{::clientInforma.recommendedCredit}}</vn-td>
<vn-td>{{::clientInforma.recommendedCredit | currency: 'EUR': 2}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>

View File

@ -0,0 +1,107 @@
module.exports = Self => {
Self.remoteMethodCtx('addFromBuy', {
description: 'Modify a field of a buy or creates a new one with default values',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The entry id',
http: {source: 'path'}
}, {
arg: 'item',
type: 'number',
required: true,
description: 'The item id',
}, {
arg: 'printedStickers',
type: 'number',
required: true,
description: 'The field to modify',
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/addFromBuy`,
verb: 'POST'
}
});
Self.addFromBuy = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {userId};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
let buy = await models.Buy.findOne({where: {entryFk: args.id}}, myOptions);
if (buy)
await buy.updateAttribute('printedStickers', args.printedStickers, myOptions);
else {
const userConfig = await models.UserConfig.findById(userId, {fields: ['warehouseFk']}, myOptions);
await Self.rawSql(
'CALL vn.buyUltimate(?,?)',
[userConfig.warehouseFk, null],
myOptions
);
let buyUltimate = await Self.rawSql(
`SELECT buyFk
FROM tmp.buyUltimate
WHERE itemFk = ?`,
[args.item],
myOptions
);
buyUltimate = await models.Buy.findById(buyUltimate[0].buyFk, null, myOptions);
buy = await models.Buy.create({
entryFk: args.id,
itemFk: args.item,
quantity: 0,
dispatched: buyUltimate.dispatched,
buyingValue: buyUltimate.buyingValue,
freightValue: buyUltimate.freightValue,
isIgnored: buyUltimate.isIgnored,
stickers: buyUltimate.stickers,
packing: buyUltimate.packing,
grouping: buyUltimate.grouping,
groupingMode: buyUltimate.groupingMode,
containerFk: buyUltimate.containerFk,
comissionValue: buyUltimate.comissionValue,
packageValue: buyUltimate.packageValue,
location: buyUltimate.location,
packageFk: buyUltimate.packageFk,
price1: buyUltimate.price1,
price2: buyUltimate.price2,
price3: buyUltimate.price3,
minPrice: buyUltimate.minPrice,
printedStickers: args.printedStickers,
workerFk: buyUltimate.workerFk,
isChecked: buyUltimate.isChecked,
isPickedOff: buyUltimate.isPickedOff,
created: buyUltimate.created,
ektFk: buyUltimate.ektFk,
weight: buyUltimate.weight,
deliveryFk: buyUltimate.deliveryFk,
itemOriginalFk: buyUltimate.itemOriginalFk
}, myOptions);
}
if (tx) await tx.commit();
return buy;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,72 @@
module.exports = Self => {
Self.remoteMethodCtx('addFromPackaging', {
description: 'Create a receipt or return entry for a supplier with a specific travel',
accessType: 'WRITE',
accepts: [{
arg: 'supplier',
type: 'number',
required: true,
description: 'The supplier id',
},
{
arg: 'isTravelReception',
type: 'boolean',
required: true,
description: 'Indicates if the travel associated with the entry is a return or receipt travel'
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/addFromPackaging`,
verb: 'POST'
}
});
Self.addFromPackaging = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const travelConfig = await models.TravelConfig.findOne({}, myOptions);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const travel = await models.Travel.create({
shipped: args.isTravelReception ? yesterday : today,
landed: args.isTravelReception ? today : tomorrow,
agencyModeFk: travelConfig.agencyFk,
warehouseInFk: travelConfig.warehouseOutFk,
warehouseOutFk: travelConfig.warehouseInFk
}, myOptions);
const entry = await models.Entry.create({
supplierFk: args.supplier,
travelFk: travel.id,
companyFk: travelConfig.companyFk
}, myOptions);
if (tx) await tx.commit();
return entry;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,51 @@
const models = require('vn-loopback/server/server').models;
describe('entry addFromBuy()', () => {
const ctx = {req: {accessToken: {userId: 18}}};
it('should change the printedStickers of an existent buy', async() => {
const id = 1;
const item = 1;
const buy = 1;
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const currentBuy = await models.Buy.findById(buy, {fields: ['printedStickers']}, options);
const printedStickers = currentBuy.printedStickers + 10;
ctx.args = {id, item, printedStickers};
const newBuy = await models.Entry.addFromBuy(ctx, options);
expect(newBuy.printedStickers).toEqual(printedStickers);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should create for an entry without a concrete item a new buy', async() => {
const id = 8;
const item = 1;
const printedStickers = 10;
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const emptyBuy = await models.Buy.findOne({where: {entryFk: id}}, options);
ctx.args = {id, item, printedStickers};
const newBuy = await models.Entry.addFromBuy(ctx, options);
expect(emptyBuy).toEqual(null);
expect(newBuy.entryFk).toEqual(id);
expect(newBuy.printedStickers).toEqual(printedStickers);
expect(newBuy.itemFk).toEqual(item);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,49 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('entry addFromPackaging()', () => {
const supplier = 442;
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 49},
http: {
req: {
headers: {origin: 'http://localhost'},
},
},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx,
});
});
it('should create an incoming travel', async() => {
const ctx = {args: {isTravelReception: true, supplier}};
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const entry = await models.Entry.addFromPackaging(ctx, options);
const travelConfig = await models.TravelConfig.findOne({}, options);
const travel = await models.Travel.findOne({order: 'id DESC'}, options);
expect(new Date(travel.shipped).getDate()).toEqual(yesterday.getDate());
expect(new Date(travel.landed).getDate()).toEqual(today.getDate());
expect(travel.agencyModeFk).toEqual(travelConfig.agencyFk);
expect(travel.warehouseInFk).toEqual(travelConfig.warehouseOutFk);
expect(travel.warehouseOutFk).toEqual(travelConfig.warehouseInFk);
expect(entry.supplierFk).toEqual(supplier);
expect(entry.travelFk).toEqual(travel.id);
expect(entry.companyFk).toEqual(travelConfig.companyFk);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -5,6 +5,9 @@
"Buy": {
"dataSource": "vn"
},
"BuyConfig": {
"dataSource": "vn"
},
"ItemMatchProperties": {
"dataSource": "vn"
},

View File

@ -0,0 +1,18 @@
{
"name": "BuyConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "buyConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"showLastBuy": {
"type": "number"
}
}
}

View File

@ -39,6 +39,9 @@
"packageValue": {
"type": "number"
},
"price1": {
"type": "number"
},
"price2": {
"type": "number"
},
@ -47,7 +50,44 @@
},
"weight": {
"type": "number"
},
"printedStickers": {
"type": "number"
},
"dispatched": {
"type": "number"
},
"isIgnored": {
"type": "boolean"
},
"containerFk": {
"type": "number"
},
"location": {
"type": "number"
},
"minPrice": {
"type": "number"
},
"isChecked": {
"type": "boolean"
},
"isPickedOff": {
"type": "boolean"
},
"created": {
"type": "date"
},
"ektFk": {
"type": "number"
},
"itemOriginalFk": {
"type": "number"
},
"editorFk": {
"type": "number"
}
},
"relations": {
"entry": {
@ -64,6 +104,16 @@
"type": "belongsTo",
"model": "Packaging",
"foreignKey": "packageFk"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"delivery": {
"type": "belongsTo",
"model": "Delivery",
"foreignKey": "deliveryFk"
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "entryLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -8,6 +8,8 @@ module.exports = Self => {
require('../methods/entry/importBuysPreview')(Self);
require('../methods/entry/lastItemBuys')(Self);
require('../methods/entry/entryOrderPdf')(Self);
require('../methods/entry/addFromPackaging')(Self);
require('../methods/entry/addFromBuy')(Self);
Self.observe('before save', async function(ctx, options) {
if (ctx.isNewInstance) return;

View File

@ -5,57 +5,5 @@
"mysql": {
"table": "invoiceInLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": [
"creationDate DESC",
"id DESC"
]
}
}

View File

@ -2,11 +2,19 @@ module.exports = Self => {
Self.remoteMethod('refund', {
description: 'Create refund tickets with sales and services if provided',
accessType: 'WRITE',
accepts: [{
arg: 'ref',
type: 'string',
description: 'The invoice reference'
}],
accepts: [
{
arg: 'ref',
type: 'string',
description: 'The invoice reference',
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
type: ['number'],
root: true
@ -17,7 +25,7 @@ module.exports = Self => {
}
});
Self.refund = async(ref, options) => {
Self.refund = async(ref, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -35,7 +43,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id);
const refundedTickets = await models.Ticket.refund(ticketsIds, myOptions);
const refundedTickets = await models.Ticket.refund(ticketsIds, withWarehouse, myOptions);
if (tx) await tx.commit();

View File

@ -3,6 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('InvoiceOut refund()', () => {
const userId = 5;
const withWarehouse = true;
const activeCtx = {
accessToken: {userId: userId},
};
@ -15,7 +16,7 @@ describe('InvoiceOut refund()', () => {
const options = {transaction: tx};
try {
const result = await models.InvoiceOut.refund('T1111111', options);
const result = await models.InvoiceOut.refund('T1111111', withWarehouse, options);
expect(result).toBeDefined();

View File

@ -76,14 +76,27 @@
translate>
Show CITES letter
</vn-item>
<vn-item
ng-click="refundConfirmation.show()"
name="refundInvoice"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-tooltip="Create a single ticket with all the content of the current invoice"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
Refund
Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refundInvoiceOut(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refundInvoiceOut(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-list>
</vn-menu>
@ -97,12 +110,7 @@
on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?">
</vn-confirm>
<vn-confirm
vn-id="refundConfirmation"
on-accept="$ctrl.refundInvoiceOut()"
question="Are you sure you want to refund this invoice?">
</vn-confirm>
<vn-client-descriptor-popover
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
@ -148,4 +156,4 @@
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
</vn-dialog>

View File

@ -114,9 +114,9 @@ class Controller extends Section {
});
}
refundInvoiceOut() {
refundInvoiceOut(withWarehouse) {
const query = 'InvoiceOuts/refund';
const params = {ref: this.invoiceOut.ref};
const params = {ref: this.invoiceOut.ref, withWarehouse: withWarehouse};
this.$http.post(query, params).then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {

View File

@ -13,10 +13,11 @@ InvoiceOut deleted: Factura eliminada
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura?
InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
Are you sure you want to refund this invoice?: Estas seguro de querer abonar esta factura?
Create a single ticket with all the content of the current invoice: Crear un ticket unico con todo el contenido de la factura actual
Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
The email can't be empty: El correo no puede estar vacío
The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}"
Refund...: Abono...

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "itemLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -23,7 +23,7 @@ class Controller extends SearchPanel {
addValue() {
this.filter.values.push({});
setTimeout(() => this.popover.relocate());
setTimeout(() => this.parentPopover.relocate());
}
changeTag() {
@ -36,7 +36,7 @@ ngModule.vnComponent('vnOrderCatalogSearchPanel', {
controller: Controller,
bindings: {
onSubmit: '&?',
popover: '<?',
parentPopover: '<?',
resultTags: '<?'
}
});

View File

@ -18,7 +18,7 @@
</vn-searchbar>
</vn-portal>
<vn-order-catalog-view
model="model"
model="model"
order="$ctrl.order">
</vn-order-catalog-view>
<vn-side-menu side="right">
@ -31,7 +31,7 @@
label="Category">
</vn-autocomplete>
<vn-one ng-repeat="category in categories">
<vn-icon
<vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}"
@ -83,7 +83,7 @@
</div>
</vn-vertical>
<vn-vertical class="input vn-pt-md">
<vn-textfield vn-one
<vn-textfield vn-one
vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag">
@ -104,20 +104,20 @@
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)"
popover="popover"
parent-popover="popover"
result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel>
</vn-popover>
<div class="chips">
<vn-chip
<vn-chip
ng-if="$ctrl.itemId"
removable="true"
vn-tooltip="Item id"
on-remove="$ctrl.removeItemId()"
class="colored">
class="colored">
<span>Id: {{$ctrl.itemId}}</span>
</vn-chip>
<vn-chip
<vn-chip
ng-if="$ctrl.itemName"
removable="true"
vn-tooltip="Item"
@ -130,20 +130,20 @@
<span>{{$ctrl.itemName}}</span>
</div>
</vn-chip>
<vn-chip
<vn-chip
ng-if="category.selection"
removable="true"
vn-tooltip="Category"
on-remove="$ctrl.categoryId = null"
class="colored">
class="colored">
<span translate>{{category.selection.name}}</span>
</vn-chip>
<vn-chip
<vn-chip
ng-if="type.selection"
removable="true"
vn-tooltip="Type"
on-remove="$ctrl.typeId = null"
class="colored">
class="colored">
<span translate>{{type.selection.name}}</span>
</vn-chip>
<vn-chip
@ -151,7 +151,7 @@
removable="true"
on-remove="$ctrl.remove($index)"
vn-tooltip="{{::$ctrl.formatTooltip(tagGroup)}}"
class="colored">
class="colored">
<div>
<span ng-if="::tagGroup.tagFk">
<span translate>{{::tagGroup.tagSelection.name}}</span>:
@ -163,4 +163,4 @@
</div>
</vn-chip>
</div>
</vn-side-menu>
</vn-side-menu>

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "routeLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -56,7 +56,7 @@
"type": "number",
"required": false
},
"printerFk": {
"mainPrinterFk": {
"type": "number",
"required": false
},
@ -69,4 +69,4 @@
"required": true
}
}
}
}

View File

@ -1,58 +1,9 @@
{
"name": "ShelvingLog",
"name": "ShelvingLog",
"base": "Log",
"options": {
"mysql": {
"table": "shelvingLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
"options": {
"mysql": {
"table": "shelvingLog"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -0,0 +1,50 @@
module.exports = Self => {
Self.remoteMethod('getItemsPackaging', {
description: 'Returns the list of items from the supplier of type packing',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The supplier id',
http: {source: 'path'}
}, {
arg: 'entry',
type: 'number',
required: true,
description: 'The entry id',
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getItemsPackaging`,
verb: 'GET'
}
});
Self.getItemsPackaging = async(id, entry) => {
return Self.rawSql(`
WITH entryTmp AS (
SELECT i.id, SUM(b.quantity) quantity
FROM vn.entry e
JOIN vn.buy b ON b.entryFk = e.id
JOIN vn.supplier s ON s.id = e.supplierFk
JOIN vn.item i ON i.id = b.itemFk
WHERE e.id = ? AND e.supplierFk = ?
GROUP BY i.id
) SELECT i.id, i.name, et.quantity, SUM(b.quantity) quantityTotal
FROM vn.buy b
JOIN vn.item i ON i.id = b.itemFk
JOIN vn.entry e ON e.id = b.entryFk
JOIN vn.supplier s ON s.id = e.supplierFk
JOIN vn.buyConfig bc ON bc.monthsAgo
JOIN vn.travel t ON t.id = e.travelFk
LEFT JOIN entryTmp et ON et.id = i.id
WHERE e.supplierFk = ?
AND i.family IN ('EMB', 'CONT')
AND b.created > (util.VN_CURDATE() - INTERVAL bc.monthsAgo MONTH)
GROUP BY b.itemFk
ORDER BY et.quantity DESC, quantityTotal DESC`, [entry, id, id]);
};
};

View File

@ -0,0 +1,12 @@
const app = require('vn-loopback/server/server');
describe('Supplier getItemsPackaging()', () => {
it('should return a summary of the list of items from a specific supplier', async() => {
const [item] = await app.models.Supplier.getItemsPackaging(1, 1);
expect(item.id).toEqual(1);
expect(item.name).toEqual('Ranged weapon longbow 2m');
expect(item.quantity).toEqual(5000);
expect(item.quantityTotal).toEqual(5100);
});
});

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "supplierLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -11,6 +11,7 @@ module.exports = Self => {
require('../methods/supplier/campaignMetricsPdf')(Self);
require('../methods/supplier/campaignMetricsEmail')(Self);
require('../methods/supplier/newSupplier')(Self);
require('../methods/supplier/getItemsPackaging')(Self);
Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty'

View File

@ -10,6 +10,11 @@ module.exports = Self => {
{
arg: 'servicesIds',
type: ['number']
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
@ -22,7 +27,7 @@ module.exports = Self => {
}
});
Self.refund = async(salesIds, servicesIds, options) => {
Self.refund = async(salesIds, servicesIds, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -129,7 +134,6 @@ module.exports = Self => {
};
async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, myOptions) {
console.log(ticketId, now, refundAgencyMode, refoundZoneId);
const models = Self.app.models;
const filter = {include: {relation: 'address'}};
@ -141,7 +145,7 @@ module.exports = Self => {
addressFk: ticket.address().id,
agencyModeFk: refundAgencyMode.id,
nickname: ticket.address().nickname,
warehouseFk: ticket.warehouseFk,
warehouseFk: withWarehouse ? ticket.warehouseFk : null,
companyFk: ticket.companyFk,
landed: now,
zoneFk: refoundZoneId

View File

@ -6,8 +6,8 @@ describe('Sale refund()', () => {
const activeCtx = {
accessToken: {userId: userId},
};
const servicesIds = [3];
const withWarehouse = true;
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
@ -22,7 +22,7 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const refundedTicket = await models.Sale.refund(salesIds, servicesIds, options);
const refundedTicket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
expect(refundedTicket).toBeDefined();
@ -40,7 +40,7 @@ describe('Sale refund()', () => {
try {
const options = {transaction: tx};
const ticket = await models.Sale.refund(salesIds, servicesIds, options);
const ticket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({
where: {

View File

@ -61,15 +61,15 @@ module.exports = Self => {
const oldQuantity = log.oldInstance.quantity;
const newQuantity = log.newInstance?.quantity || 0;
if (oldQuantity || newQuantity) {
const changeMessage = $t('Change quantity', {
concept: log.changedModelValue,
oldQuantity: oldQuantity || 0,
newQuantity: newQuantity || 0,
});
changes.push(changeMessage);
if (oldQuantity > newQuantity) {
const changeMessage = $t('Change quantity', {
concept: log.changedModelValue,
oldQuantity: oldQuantity || 0,
newQuantity: newQuantity || 0,
});
changes.push(changeMessage);
}
}
}
return changes.join('\n');
};

View File

@ -7,7 +7,7 @@ describe('ticketLog getChanges()', () => {
return value;
};
it('should return the changes in the sales of a ticket', async() => {
const ticketId = 7;
const ticketId = 16;
const changues = await models.TicketLog.getChanges(ctx, ticketId);

View File

@ -7,6 +7,11 @@ module.exports = Self => {
arg: 'ticketsIds',
type: ['number'],
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
@ -19,7 +24,7 @@ module.exports = Self => {
}
});
Self.refund = async(ticketsIds, options) => {
Self.refund = async(ticketsIds, withWarehouse, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -41,7 +46,7 @@ module.exports = Self => {
const services = await models.TicketService.find(filter, myOptions);
const servicesIds = services.map(service => service.id);
const refundedTickets = await models.Sale.refund(salesIds, servicesIds, myOptions);
const refundedTickets = await models.Sale.refund(salesIds, servicesIds, withWarehouse, myOptions);
if (tx) await tx.commit();

View File

@ -1,58 +1,9 @@
{
"name": "TicketLog",
"name": "TicketLog",
"base": "Log",
"options": {
"mysql": {
"table": "ticketLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
"options": {
"mysql": {
"table": "ticketLog"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -150,7 +150,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState | dashIfEmpty}}
</span>
</td>

View File

@ -102,13 +102,6 @@ export default class Controller extends Section {
return checkedLines;
}
stateColor(state) {
if (state === 'OK')
return 'success';
else if (state === 'Libre')
return 'notice';
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);

View File

@ -61,24 +61,6 @@ describe('Component vnTicketAdvance', () => {
});
});
describe('stateColor()', () => {
it('should return success to the OK tickets', () => {
const ok = controller.stateColor(controller.$.model.data[0].state);
const notOk = controller.stateColor(controller.$.model.data[1].state);
expect(ok).toEqual('success');
expect(notOk).not.toEqual('success');
});
it('should return success to the FREE tickets', () => {
const notFree = controller.stateColor(controller.$.model.data[0].state);
const free = controller.stateColor(controller.$.model.data[1].state);
expect(free).toEqual('notice');
expect(notFree).not.toEqual('notice');
});
});
describe('dateRange()', () => {
it('should return two dates with the hours at the start and end of the given date', () => {
const now = Date.vnNew();

View File

@ -141,12 +141,27 @@
translate>
Recalculate components
</vn-item>
<vn-item
ng-click="refundAllConfirmation.show()"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
vn-tooltip="Create a single ticket with all the content of the current ticket"
translate>
Refund all
Refund all...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refund(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-list>
</vn-menu>
@ -319,14 +334,6 @@
message="Recalculate components">
</vn-confirm>
<!-- Refund all confirmation dialog -->
<vn-confirm
vn-id="refundAllConfirmation"
on-accept="$ctrl.refund()"
question="Are you sure you want to refund all?"
message="Refund all">
</vn-confirm>
<!-- Client balance popup-->
<vn-client-balance-create
vn-id="balance-create"

View File

@ -297,16 +297,17 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
async refund() {
const params = {ticketsIds: [this.id]};
refund(withWarehouse) {
const params = {ticketsIds: [this.id], withWarehouse: withWarehouse};
const query = 'Tickets/refund';
return this.$http.post(query, params).then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));
this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
return this.$http.post(query, params)
.then(res => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id
}));
this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
}
onSmsSend(sms) {

View File

@ -10,7 +10,9 @@ Send CSV: Enviar CSV
Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF
Show Proforma: Ver proforma
Refund all: Abonar todo
Refund all...: Abonar todo...
with warehouse: con almacén
without warehouse: sin almacén
Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente
@ -18,3 +20,4 @@ SMS Notify changes: SMS Notificar cambios
PDF sent!: ¡PDF enviado!
Already exist signed delivery note: Ya existe albarán de entrega firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán de entrega?
Create a single ticket with all the content of the current ticket: Crea un ticket único con todo el contenido del ticket actual

View File

@ -23,4 +23,4 @@ Restore ticket: Restaurar ticket
You are going to restore this ticket: Vas a restaurar este ticket
Are you sure you want to restore this ticket?: ¿Seguro que quieres restaurar el ticket?
Are you sure you want to refund all?: ¿Seguro que quieres abonar todo?
Send changes: "Verdnatura le recuerda:\rPedido {{ticketId}} día {{created | date: 'dd/MM/yyyy'}}\r{{changes}}"
Send changes: "Verdnatura:\rPedido {{ticketId}}\r{{changes}}"

View File

@ -158,7 +158,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState}}
</span>
</td>

View File

@ -529,11 +529,28 @@
ng-if="$ctrl.isEditable && $ctrl.hasReserves()">
Unmark as reserved
</vn-item>
<vn-item translate
name="refund"
ng-click="$ctrl.createRefund()"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove">
Refund
</vn-item>
<vn-item class="dropdown"
name="refund"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
name="refundWithWarehouse"
ng-click="$ctrl.createRefund(true)"
translate>
with warehouse
</vn-item>
<vn-item
name="refundWithoutWarehouse"
ng-click="$ctrl.createRefund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-menu>

View File

@ -520,13 +520,12 @@ class Controller extends Section {
});
}
createRefund() {
createRefund(withWarehouse) {
const sales = this.selectedValidSales();
if (!sales) return;
const salesIds = sales.map(sale => sale.id);
const params = {salesIds: salesIds};
const params = {salesIds: salesIds, withWarehouse: withWarehouse};
const query = 'Sales/refund';
this.$http.post(query, params).then(res => {
const refundTicket = res.data;

View File

@ -36,10 +36,10 @@ Warehouse: Almacen
Agency: Agencia
Shipped: F. envio
Packaging: Encajado
Refund: Abono
Refund...: Abono...
Promotion mana: Maná promoción
Claim mana: Maná reclamación
History: Historial
Do you want to continue?: ¿Desea continuar?
Claim out of time: Reclamación fuera de plazo
Do you want to create a claim?: ¿Quieres crear una reclamación?
Do you want to create a claim?: ¿Quieres crear una reclamación?

View File

@ -14,6 +14,9 @@
"TravelThermograph": {
"dataSource": "vn"
},
"TravelConfig": {
"dataSource": "vn"
},
"Temperature": {
"dataSource": "vn"
}

View File

@ -0,0 +1,49 @@
{
"name": "TravelConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "travelConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"warehouseInFk": {
"type": "number"
},
"warehouseOutFk": {
"type": "number"
},
"agencyFk": {
"type": "number"
},
"companyFk": {
"type": "number"
}
},
"relations": {
"warehouseIn": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseInFk"
},
"warehouseOut": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseOutFk"
},
"agency": {
"type": "belongsTo",
"model": "AgencyMode",
"foreignKey": "agencyFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "travelLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -0,0 +1,62 @@
const models = require('vn-loopback/server/server').models;
describe('Operator', () => {
const authorFk = 9;
const sectorId = 1;
const mainPrinter = 1;
const notificationName = 'not-main-printer-configured';
const operator = {
workerFk: 1,
trainFk: 1,
itemPackingTypeFk: 'H',
warehouseFk: 1,
sectorFk: sectorId
};
async function createOperator(labelerFk, options) {
operator.labelerFk = labelerFk;
await models.Operator.create(operator, options);
return models.NotificationQueue.findOne({
where: {
notificationFk: notificationName
}
}, options);
}
it('should create notification when configured a not main printer in the sector', async() => {
const tx = await models.Operator.beginTransaction({});
try {
const options = {transaction: tx, accessToken: {userId: authorFk}};
const notificationQueue = await createOperator(2, options);
const params = JSON.parse(notificationQueue.params);
expect(notificationQueue.notificationFk).toEqual(notificationName);
expect(notificationQueue.authorFk).toEqual(authorFk);
expect(params.labelerId).toEqual(2);
expect(params.sectorId).toEqual(1);
expect(params.workerId).toEqual(9);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should not create notification when configured the main printer in the sector', async() => {
const tx = await models.Operator.beginTransaction({});
try {
const options = {transaction: tx, accessToken: {userId: authorFk}};
const notificationQueue = await createOperator(mainPrinter, options);
expect(notificationQueue).toEqual(null);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -1,55 +1,14 @@
{
"name": "DeviceProductionLog",
"base": "Log",
"options": {
"mysql": {
"table": "deviceProductionLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"deviceProduction": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"created": {
"type": "date"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"changedModel": {
"type": "string"
},
"changedModelId": {
"type": "number"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
"name": "DeviceProductionLog",
"base": "Log",
"options": {
"mysql": {
"table": "deviceProductionLog"
}
},
"scope": {
"order": ["created DESC", "id DESC"]
"properties": {
"deviceProduction": {
"type": "number"
}
}
}

View File

@ -0,0 +1,28 @@
module.exports = function(Self) {
Self.observe('after save', async function(ctx) {
const instance = ctx.instance;
const models = Self.app.models;
const options = ctx.options;
if (!instance.sectorFk || !instance.labelerFk) return;
const sector = await models.Sector.findById(instance.sectorFk, {
fields: ['mainPrinterFk']
}, options);
if (sector.mainPrinterFk && sector.mainPrinterFk != instance.labelerFk) {
const userId = ctx.options.accessToken.userId;
await models.NotificationQueue.create({
notificationFk: 'not-main-printer-configured',
authorFk: userId,
params: JSON.stringify(
{
'labelerId': instance.labelerFk,
'sectorId': instance.sectorFk,
'workerId': userId
}
)
}, options);
}
});
};

View File

@ -27,10 +27,10 @@
"type": "number",
"required": true
},
"sectorFk ": {
"sectorFk": {
"type": "number"
},
"labelerFk ": {
"labelerFk": {
"type": "number"
}
},
@ -41,4 +41,4 @@
"foreignKey": "sectorFk"
}
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "workerLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"table": "zoneLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "salix-back",
"version": "23.22.01",
"version": "23.24.01",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "salix-back",
"version": "23.22.01",
"version": "23.24.01",
"license": "GPL-3.0",
"dependencies": {
"axios": "^1.2.2",

View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.24.01",
"version": "23.26.01",
"author": "Verdnatura Levante SL",
"description": "Salix backend",
"license": "GPL-3.0",

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,3 @@
subject: Not main printer configured
title: Not main printer configured
description: 'Printer #{0} {1} has been configured in sector #{2} {3} (the main printer for that sector is #{4} {5}). Ask the worker {6}.'

View File

@ -0,0 +1,3 @@
subject: Configurada impresora no principal
title: Configurada impresora no principal
description: 'Se ha configurado la impresora #{0} {1} en el sector #{2} {3} (la impresora principal de ese sector es la #{4} {5}). Preguntar al trabajador {6}.'

View File

@ -0,0 +1,8 @@
<email-body v-bind="$props">
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p v-html="$t('description', [labeler.id, labeler.name, sector.id, sector.description, mainPrinter.id, mainPrinter.name, worker.nickname])"></p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,33 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'not-main-printer-configured',
async serverPrefetch() {
this.sector = await this.findOneFromDef('sector', [this.sectorId]);
if (!this.sector)
throw new Error('Something went wrong');
this.labeler = await this.findOneFromDef('printer', [this.labelerId]);
this.mainPrinter = await this.findOneFromDef('printer', [this.sector.mainPrinterFk]);
this.worker = await this.findOneFromDef('worker', [this.workerId]);
},
components: {
'email-body': emailBody.build(),
},
props: {
labelerId: {
type: Number,
required: true
},
sectorId: {
type: Number,
required: true
},
workerId: {
type: Number,
required: true
}
}
};

View File

@ -0,0 +1,3 @@
SELECT id, name
FROM vn.printer
WHERE id = ?

View File

@ -0,0 +1,3 @@
SELECT id, description, mainPrinterFk
FROM vn.sector
WHERE id = ?

Some files were not shown because too many files have changed in this diff Show More