Merge branch 'test' of https://gitea.verdnatura.es/verdnatura/salix
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Joan Sanchez 2022-02-15 13:36:06 +01:00
commit 9a1b90eec4
203 changed files with 7212 additions and 3703 deletions

View File

@ -1,4 +0,0 @@
DELETE FROM `salix`.`ACL` WHERE id = 48;
DELETE FROM `salix`.`ACL` WHERE id = 49;
DELETE FROM `salix`.`ACL` WHERE id = 50;
DELETE FROM `salix`.`ACL` WHERE id = 107;

View File

@ -1,197 +0,0 @@
drop procedure `vn`.`sale_getProblems`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`sale_getProblems`(IN vIsTodayRelative tinyint(1))
BEGIN
/**
* Calcula los problemas de cada venta
* para un conjunto de tickets.
*
* @table tmp.sale_getProblems(ticketFk, clientFk, warehouseFk, shipped) Identificadores de los tickets a calcular
* @return tmp.sale_problems
*/
DECLARE vWarehouse INT;
DECLARE vDate DATE;
DECLARE vAvailableCache INT;
DECLARE vDone INT DEFAULT 0;
DECLARE vComponentCount INT;
DECLARE vCursor CURSOR FOR
SELECT DISTINCT tt.warehouseFk, IF(vIsTodayRelative, CURDATE(), date(tt.shipped))
FROM tmp.sale_getProblems tt
WHERE DATE(tt.shipped) BETWEEN CURDATE()
AND TIMESTAMPADD(DAY, IF(vIsTodayRelative, 9.9, 1.9), CURDATE());
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = 1;
DROP TEMPORARY TABLE IF EXISTS tmp.sale_problems;
CREATE TEMPORARY TABLE tmp.sale_problems (
ticketFk INT(11),
saleFk INT(11),
isFreezed INTEGER(1) DEFAULT 0,
risk DECIMAL(10,2) DEFAULT 0,
hasHighRisk TINYINT(1) DEFAULT 0,
hasTicketRequest INTEGER(1) DEFAULT 0,
isAvailable INTEGER(1) DEFAULT 1,
itemShortage VARCHAR(250),
isTaxDataChecked INTEGER(1) DEFAULT 1,
itemDelay VARCHAR(250),
hasComponentLack INTEGER(1),
PRIMARY KEY (ticketFk, saleFk)
) ENGINE = MEMORY;
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_list;
CREATE TEMPORARY TABLE tmp.ticket_list
(PRIMARY KEY (ticketFk))
ENGINE = MEMORY
SELECT tp.ticketFk, c.id clientFk
FROM tmp.sale_getProblems tp
JOIN vn.client c ON c.id = tp.clientFk;
SELECT COUNT(*) INTO vComponentCount
FROM vn.component c
WHERE c.isRequired;
INSERT INTO tmp.sale_problems(ticketFk, hasComponentLack, saleFk)
SELECT tl.ticketFk, (COUNT(DISTINCT s.id) * vComponentCount > COUNT(c.id)), s.id
FROM tmp.ticket_list tl
JOIN vn.sale s ON s.ticketFk = tl.ticketFk
LEFT JOIN vn.saleComponent sc ON sc.saleFk = s.id
LEFT JOIN vn.component c ON c.id = sc.componentFk AND c.isRequired
JOIN vn.ticket t ON t.id = tl.ticketFk
JOIN vn.agencyMode am ON am.id = t.agencyModeFk
JOIN vn.deliveryMethod dm ON dm.id = am.deliveryMethodFk
WHERE dm.code IN('AGENCY','DELIVERY','PICKUP')
GROUP BY tl.ticketFk, s.id;
INSERT INTO tmp.sale_problems(ticketFk, isFreezed)
SELECT DISTINCT tl.ticketFk, TRUE
FROM tmp.ticket_list tl
JOIN vn.client c ON c.id = tl.clientFk
WHERE c.isFreezed
ON DUPLICATE KEY UPDATE
isFreezed = c.isFreezed;
DROP TEMPORARY TABLE IF EXISTS tmp.clientGetDebt;
CREATE TEMPORARY TABLE tmp.clientGetDebt
(PRIMARY KEY (clientFk))
ENGINE = MEMORY
SELECT DISTINCT clientFk
FROM tmp.ticket_list;
CALL clientGetDebt(CURDATE());
INSERT INTO tmp.sale_problems(ticketFk, risk, hasHighRisk)
SELECT DISTINCT tl.ticketFk, r.risk, ((r.risk - cc.riskTolerance) > c.credit + 10)
FROM tmp.ticket_list tl
JOIN vn.ticket t ON t.id = tl.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN tmp.risk r ON r.clientFk = t.clientFk
JOIN vn.client c ON c.id = t.clientFk
JOIN vn.clientConfig cc
WHERE r.risk > c.credit + 10
AND a.isRiskFree = FALSE
ON DUPLICATE KEY UPDATE
risk = r.risk, hasHighRisk = ((r.risk - cc.riskTolerance) > c.credit + 10);
INSERT INTO tmp.sale_problems(ticketFk, hasTicketRequest)
SELECT DISTINCT tl.ticketFk, TRUE
FROM tmp.ticket_list tl
JOIN vn.ticketRequest tr ON tr.ticketFk = tl.ticketFk
WHERE tr.isOK IS NULL
ON DUPLICATE KEY UPDATE
hasTicketRequest = TRUE;
OPEN vCursor;
WHILE NOT vDone
DO
FETCH vCursor INTO vWarehouse, vDate;
CALL cache.available_refresh(vAvailableCache, FALSE, vWarehouse, vDate);
INSERT INTO tmp.sale_problems(ticketFk, isAvailable, saleFk)
SELECT tl.ticketFk, FALSE, s.id
FROM tmp.ticket_list tl
JOIN vn.ticket t ON t.id = tl.ticketFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it on it.id = i.typeFk
LEFT JOIN cache.available av ON av.item_id = i.id
AND av.calc_id = vAvailableCache
WHERE date(t.shipped) = vDate
AND it.categoryFk != 6
AND IFNULL(av.available, 0) < 0
AND s.isPicked = FALSE
AND NOT i.generic
AND vWarehouse = t.warehouseFk
GROUP BY tl.ticketFk
ON DUPLICATE KEY UPDATE
isAvailable = FALSE, saleFk = VALUES(saleFk);
INSERT INTO tmp.sale_problems(ticketFk, itemShortage, saleFk)
SELECT ticketFk, problem, saleFk
FROM (
SELECT tl.ticketFk, CONCAT('F: ',GROUP_CONCAT(i.id, ' ', i.longName, ' ')) problem, s.id AS saleFk
FROM tmp.ticket_list tl
JOIN vn.ticket t ON t.id = tl.ticketFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it on it.id = i.typeFk
LEFT JOIN vn.itemShelvingStock_byWarehouse issw ON issw.itemFk = i.id AND issw.warehouseFk = t.warehouseFk
LEFT JOIN cache.available av ON av.item_id = i.id AND av.calc_id = vAvailableCache
WHERE IFNULL(av.available, 0) < 0
AND s.quantity > IFNULL(issw.visible, 0)
AND s.quantity > 0
AND s.isPicked = FALSE
AND s.reserved = FALSE
AND it.categoryFk != 6
AND IF(vIsTodayRelative, TRUE, date(t.shipped) = vDate)
AND NOT i.generic
AND CURDATE() = vDate
AND t.warehouseFk = vWarehouse
GROUP BY tl.ticketFk LIMIT 1) sub
ON DUPLICATE KEY UPDATE
itemShortage = sub.problem, saleFk = sub.saleFk;
INSERT INTO tmp.sale_problems(ticketFk, itemDelay, saleFk)
SELECT ticketFk, problem, saleFk
FROM (
SELECT tl.ticketFk, GROUP_CONCAT('I: ',i.id, ' ', i.longName, ' ') problem, s.id AS saleFk
FROM tmp.ticket_list tl
JOIN vn.ticket t ON t.id = tl.ticketFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it on it.id = i.typeFk
LEFT JOIN vn.itemShelvingStock_byWarehouse issw ON issw.itemFk = i.id AND issw.warehouseFk = t.warehouseFk
WHERE s.quantity > IFNULL(issw.visible, 0)
AND s.quantity > 0
AND s.isPicked = FALSE
AND s.reserved = FALSE
AND it.categoryFk != 6
AND IF(vIsTodayRelative, TRUE, date(t.shipped) = vDate)
AND NOT i.generic
AND CURDATE() = vDate
AND t.warehouseFk = vWarehouse
GROUP BY tl.ticketFk LIMIT 1) sub
ON DUPLICATE KEY UPDATE
itemDelay = sub.problem, saleFk = sub.saleFk;
END WHILE;
CLOSE vCursor;
INSERT INTO tmp.sale_problems(ticketFk, isTaxDataChecked)
SELECT DISTINCT tl.ticketFk, FALSE
FROM tmp.ticket_list tl
JOIN vn.client c ON c.id = tl.clientFk
WHERE c.isTaxDataChecked = FALSE
ON DUPLICATE KEY UPDATE
isTaxDataChecked = FALSE;
DROP TEMPORARY TABLE
tmp.clientGetDebt,
tmp.ticket_list;
END;;$$
DELIMITER ;

View File

@ -1,48 +0,0 @@
drop procedure `vn`.`ticket_getProblems`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`ticket_getProblems`(IN vIsTodayRelative tinyint(1))
BEGIN
/**
* Calcula los problemas para un conjunto de tickets.
* Agrupados por ticket
*
* @table tmp.sale_getProblems(ticketFk, clientFk, warehouseFk, shipped) Identificadores de los tickets a calcular
* @return tmp.ticket_problems
*/
CALL sale_getProblems(vIsTodayRelative);
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_problems;
CREATE TEMPORARY TABLE tmp.ticket_problems
(INDEX (ticketFk))
ENGINE = MEMORY
SELECT
ticketFk,
MAX(p.isFreezed) AS isFreezed,
MAX(p.risk) AS risk,
MAX(p.hasHighRisk) AS hasHighRisk,
MAX(p.hasTicketRequest) AS hasTicketRequest,
MIN(p.isAvailable) AS isAvailable,
MAX(p.itemShortage) AS itemShortage,
MIN(p.isTaxDataChecked) AS isTaxDataChecked,
MAX(p.hasComponentLack) AS hasComponentLack,
0 AS totalProblems
FROM tmp.sale_problems p
GROUP BY ticketFk;
UPDATE tmp.ticket_problems tp
SET tp.totalProblems = (
(tp.isFreezed) +
IF(tp.risk, TRUE, FALSE) +
(tp.hasTicketRequest) +
(tp.isAvailable = 0) +
(tp.isTaxDataChecked = 0) +
(tp.hasComponentLack)
);
DROP TEMPORARY TABLE
tmp.sale_problems;
END;;$$
DELIMITER ;

View File

@ -1 +0,0 @@
alter table `vn`.`travelThermograph` modify `temperature` enum('COOL', 'WARM') null;

View File

@ -1 +0,0 @@
UPDATE salix.ACL t SET t.principalId = 'employee' WHERE t.id = 269;

View File

@ -1,3 +0,0 @@
ALTER TABLE vn.accountingType ADD maxAmount INT DEFAULT NULL NULL;
UPDATE vn.accountingType SET maxAmount = 1000 WHERE code = 'cash';

View File

@ -1,4 +0,0 @@
ALTER TABLE vn.payMethod CHANGE ibanRequired ibanRequiredForClients tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod ADD ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod CHANGE ibanRequiredForSuppliers ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL AFTER ibanRequiredForClients;
UPDATE vn.payMethod SET ibanRequiredForSuppliers = 1 WHERE code = 'wireTransfer';

View File

@ -1,2 +0,0 @@
ALTER TABLE vn.silexACL MODIFY module VARCHAR(50) NOT NULL;
ALTER TABLE vn.silexACL MODIFY method VARCHAR(50) NOT NULL;

View File

@ -1,48 +0,0 @@
drop procedure `vn`.`ticket_getProblems`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`ticket_getProblems`(IN vIsTodayRelative tinyint(1))
BEGIN
/**
* Calcula los problemas para un conjunto de tickets.
* Agrupados por ticket
*
* @table tmp.sale_getProblems(ticketFk, clientFk, warehouseFk, shipped) Identificadores de los tickets a calcular
* @return tmp.ticket_problems
*/
CALL sale_getProblems(vIsTodayRelative);
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_problems;
CREATE TEMPORARY TABLE tmp.ticket_problems
(PRIMARY KEY (ticketFk))
ENGINE = MEMORY
SELECT
ticketFk,
MAX(p.isFreezed) AS isFreezed,
MAX(p.risk) AS risk,
MAX(p.hasHighRisk) AS hasHighRisk,
MAX(p.hasTicketRequest) AS hasTicketRequest,
MIN(p.isAvailable) AS isAvailable,
MAX(p.itemShortage) AS itemShortage,
MIN(p.isTaxDataChecked) AS isTaxDataChecked,
MAX(p.hasComponentLack) AS hasComponentLack,
0 AS totalProblems
FROM tmp.sale_problems p
GROUP BY ticketFk;
UPDATE tmp.ticket_problems tp
SET tp.totalProblems = (
(tp.isFreezed) +
IF(tp.risk, TRUE, FALSE) +
(tp.hasTicketRequest) +
(tp.isAvailable = 0) +
(tp.isTaxDataChecked = 0) +
(tp.hasComponentLack)
);
DROP TEMPORARY TABLE
tmp.sale_problems;
END;;$$
DELIMITER ;

View File

@ -1,14 +0,0 @@
CREATE TABLE `salix`.`defaultViewConfig`
(
tableCode VARCHAR(25) not null,
columns JSON not null
)
comment 'The default configuration of columns for views';
INSERT INTO `salix`.`defaultViewConfig` (tableCode, columns)
VALUES
('itemsIndex', '{"intrastat":false,"stemMultiplier":false,"landed":false}'),
('latestBuys', '{"intrastat":false,"description":false,"density":false,"isActive":false,"freightValue":false,"packageValue":false,"isIgnored":false,"price2":false,"minPrice":true,"ektFk":false,"weight":false,"id":true,"packing":true,"grouping":true,"quantity":true,"size":false,"name":true,"code":true,"origin":true,"family":true,"entryFk":true,"buyingValue":true,"comissionValue":false,"price3":true,"packageFk":true,"packingOut":true}'),
('ticketsMonitor', '{"id":false}');

View File

@ -1,5 +0,0 @@
ALTER TABLE `vn`.`smsConfig` ADD apiKey varchar(50) NULL;
ALTER TABLE `vn`.`smsConfig` CHANGE `user` user__ varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL;
ALTER TABLE `vn`.`smsConfig` CHANGE password password__ varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL;
ALTER TABLE `vn`.`sms` MODIFY COLUMN statusCode smallint(9) DEFAULT 0 NULL;
ALTER TABLE `vn`.`sms` MODIFY COLUMN status varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT 'OK' NULL;

View File

@ -1,7 +0,0 @@
UPDATE `vn`.`smsConfig`
SET `uri` = 'https://api.gateway360.com/api/3.0/sms/send'
WHERE `id` = 1;
UPDATE `vn`.`smsConfig`
SET `apiKey` = '5715476da95b46d686a5a255e6459523'
WHERE `id` = 1;

View File

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

View File

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

View File

@ -0,0 +1,12 @@
UPDATE `vn`.`companyGroup`
SET `code`='verdnatura'
WHERE `id`=1;
UPDATE `vn`.`companyGroup`
SET `code`='ornamental'
WHERE `id`=2;
UPDATE `vn`.`companyGroup`
SET `code`='other'
WHERE `id`=3;
UPDATE `vn`.`companyGroup`
SET `code`='provisional'
WHERE `id`=4;

View File

@ -0,0 +1,38 @@
USE vn;
DELIMITER $$
$$
CREATE OR REPLACE
ALGORITHM = UNDEFINED VIEW `vn`.`saleVolume` AS
select
`s`.`ticketFk` AS `ticketFk`,
`s`.`id` AS `saleFk`,
round(`ic`.`cm3delivery` * `s`.`quantity` / 1000, 0) AS `litros`,
`t`.`routeFk` AS `routeFk`,
`t`.`shipped` AS `shipped`,
`t`.`landed` AS `landed`,
`s`.`quantity` * `ic`.`cm3delivery` / 1000000 AS `volume`,
`s`.`quantity` * `ic`.`grams` / 1000 AS `physicalWeight`,
`s`.`quantity` * `ic`.`cm3delivery` * greatest(`i`.`density`, 167) / 1000000 AS `weight`,
`s`.`quantity` * `ic`.`cm3delivery` / 1000000 AS `physicalVolume`,
`s`.`quantity` * `ic`.`cm3delivery` * ifnull(`t`.`zonePrice`, `z`.`price`) / (`vc`.`standardFlowerBox` * 1000) AS `freight`,
`t`.`zoneFk` AS `zoneFk`,
`t`.`clientFk` AS `clientFk`,
`s`.`isPicked` AS `isPicked`,
`s`.`quantity` * `s`.`price` * (100 - `s`.`discount`) / 100 AS `eurosValue`,
`i`.`itemPackingTypeFk` AS `itemPackingTypeFk`
from
(((((`sale` `s`
join `item` `i` on
(`i`.`id` = `s`.`itemFk`))
join `ticket` `t` on
(`t`.`id` = `s`.`ticketFk`))
join `zone` `z` on
(`z`.`id` = `t`.`zoneFk`))
join `volumeConfig` `vc`)
join `itemCost` `ic` on
(`ic`.`itemFk` = `s`.`itemFk`
and `ic`.`warehouseFk` = `t`.`warehouseFk`))
where
`s`.`quantity` > 0;
$$
DELIMITER ;

View File

@ -0,0 +1,43 @@
DROP PROCEDURE IF EXISTS `vn`.`ticket_getMovable`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`ticket_getMovable`(vTicketFk INT, vDatedNew DATETIME, vWarehouseFk INT)
BEGIN
/**
* Cálcula el stock movible para los artículos de un ticket
*
* @param vTicketFk -> Ticket
* @param vDatedNew -> Nueva fecha
* @return Sales con Movible
*/
DECLARE vDatedOld DATETIME;
SELECT t.shipped INTO vDatedOld
FROM ticket t
WHERE t.id = vTicketFk;
CALL itemStock(vWarehouseFk, DATE_SUB(vDatedNew, INTERVAL 1 DAY), NULL);
CALL item_getMinacum(vWarehouseFk, vDatedNew, DATEDIFF(vDatedOld, vDatedNew), NULL);
SELECT s.id,
s.itemFk,
s.quantity,
s.concept,
s.price,
s.reserved,
s.discount,
i.image,
i.subName,
il.stock + IFNULL(im.amount, 0) AS movable
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
LEFT JOIN tmp.itemMinacum im ON im.itemFk = s.itemFk AND im.warehouseFk = vWarehouseFk
LEFT JOIN tmp.itemList il ON il.itemFk = s.itemFk
WHERE t.id = vTicketFk;
DROP TEMPORARY TABLE IF EXISTS tmp.itemList;
DROP TEMPORARY TABLE IF EXISTS tmp.itemMinacum;
END$$
DELIMITER ;

View File

@ -0,0 +1,25 @@
ALTER TABLE `postgresql`.`business` ADD payedHolidays INT NULL;
ALTER TABLE `postgresql`.`business` CHANGE payedHolidays payedHolidays INT NULL AFTER reasonEndFk;
CREATE OR REPLACE
ALGORITHM = UNDEFINED VIEW `vn`.`workerLabour` AS
select
`b`.`business_id` AS `businessFk`,
`p`.`id_trabajador` AS `workerFk`,
`bl`.`workcenter_id` AS `workCenterFk`,
`b`.`date_start` AS `started`,
`b`.`date_end` AS `ended`,
`d`.`id` AS `departmentFk`,
`b`.`payedHolidays` AS `payedHolidays`
from
((((`postgresql`.`person` `p`
join `postgresql`.`profile` `pr` on
((`pr`.`person_id` = `p`.`person_id`)))
join `postgresql`.`business` `b` on
((`b`.`client_id` = `pr`.`profile_id`)))
join `postgresql`.`business_labour` `bl` on
((`b`.`business_id` = `bl`.`business_id`)))
join `vn`.`department` `d` on
((`d`.`id` = `bl`.`department_id`)))
order by
`b`.`date_start` desc

View File

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

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@ ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1;
INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES
('TOTALLY_SECURE_TOKEN', '1209600', CURDATE(), 66);
('DEFAULT_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
@ -104,17 +104,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
(3, 'GBP', 'Libra', 1),
(4, 'JPY', 'Yen Japones', 1);
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`)
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES
(1, 'España', 1, 'ES', 1, 24, 4),
(2, 'Italia', 1, 'IT', 1, 27, 4),
(3, 'Alemania', 1, 'DE', 1, 22, 4),
(4, 'Rumania', 1, 'RO', 1, 24, 4),
(5, 'Holanda', 1, 'NL', 1, 18, 4),
(8, 'Portugal', 1, 'PT', 1, 27, 4),
(13,'Ecuador', 0, 'EC', 1, 24, 2),
(19,'Francia', 1, 'FR', 1, 27, 4),
(30,'Canarias', 1, 'IC', 1, 24, 4);
(1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
(3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
(4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
(5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
(8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
(13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
(19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
(30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`)
VALUES
@ -243,7 +243,7 @@ INSERT INTO `vn`.`province`(`id`, `name`, `countryFk`, `autonomyFk`, `warehouseF
VALUES
(1, 'Province one', 1, 1, NULL),
(2, 'Province two', 1, 1, NULL),
(3, 'Province three', 1, 2, NULL),
(3, 'Province three', 30, 2, NULL),
(4, 'Province four', 2, 3, NULL),
(5, 'Province five', 13, 4, NULL);
@ -455,7 +455,8 @@ INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `crea
INSERT INTO `vn`.`companyGroup`(`id`, `code`)
VALUES
(1, 'Wayne Industries');
(1, 'wayneIndustries'),
(2, 'verdnatura');
INSERT INTO `vn`.`bankEntity`(`id`, `countryFk`, `name`, `bic`)
VALUES
@ -466,13 +467,13 @@ INSERT INTO `vn`.`supplierAccount`(`id`, `supplierFk`, `iban`, `bankEntityFk`)
VALUES
(241, 442, 'ES111122333344111122221111', 128);
INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `phytosanitary`)
INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `companyGroupFk`, `phytosanitary`)
VALUES
(69 , 'CCs', NULL, 30, NULL, 0, NULL, NULL),
(442 , 'VNL', 241, 30, 2 , 1, NULL, 'VNL Company - Plant passport'),
(567 , 'VNH', NULL, 30, NULL, 4, NULL, 'VNH Company - Plant passport'),
(791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', NULL),
(1381, 'ORN', NULL, 30, NULL, 7, NULL, 'ORN Company - Plant passport');
(69 , 'CCs', NULL, 30, NULL, 0, NULL, 1, NULL),
(442 , 'VNL', 241, 30, 2 , 1, NULL, 2, 'VNL Company - Plant passport'),
(567 , 'VNH', NULL, 30, NULL, 4, NULL, 1, 'VNH Company - Plant passport'),
(791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', 1, NULL),
(1381, 'ORN', NULL, 30, NULL, 7, NULL, 1, 'ORN Company - Plant passport');
INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`)
VALUES
@ -486,7 +487,9 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
('A', 'Global nacional', 1, 'NATIONAL', 0),
('T', 'Española rapida', 1, 'NATIONAL', 0),
('V', 'Intracomunitaria global', 0, 'CEE', 1),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0);
('M', 'Múltiple nacional', 1, 'NATIONAL', 0),
('E', 'Exportación rápida', 0, 'WORLD', 0);
;
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES
@ -622,6 +625,7 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF
(25 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, CURDATE()),
(26 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, CURDATE()),
(27 ,NULL, 8, 1, NULL, CURDATE(), CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, CURDATE());
INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`)
VALUES
(1, 11, 1, 'ready'),
@ -799,25 +803,25 @@ INSERT INTO `vn`.`itemFamily`(`code`, `description`)
('VT', 'Sales');
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`,
`comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`)
`comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`)
VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT', 0, NULL),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT', 0, NULL),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT', 0, NULL),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT', 0, NULL),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT', 1, NULL),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT', 0, NULL),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT', 0, NULL),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT', 0, NULL),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 0, 2, 'VT', 1, NULL),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT', 1, NULL),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL),
(16, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL),
(71, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT', 0, NULL);
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL, 'V'),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H'),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '5', NULL, 0, 3, 'VT', 0, NULL, NULL),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '6', NULL, 0, 4, 'VT', 0, NULL, NULL),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '7', NULL, 0, 4, 'VT', 0, NULL, NULL),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '8', NULL, 0, 5, 'VT', 0, NULL, NULL),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '9', NULL, 0, 4, 'VT', 1, NULL, NULL),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '10', NULL, 0, 4, 'VT', 0, NULL, NULL),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 4751000000, NULL, 0, '11', NULL, 0, 4, 'VT', 0, NULL, NULL),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 4751000000, NULL, 0, '12', NULL, 0, 3, 'VT', 0, NULL, NULL),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '13', NULL, 0, 2, 'VT', 1, NULL, NULL),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 4, 'VT', 1, NULL, NULL),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL),
(16, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'EMB', 0, NULL, NULL),
(71, 6, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 4751000000, NULL, 0, '', NULL, 0, 0, 'VT', 0, NULL, NULL);
-- Update the taxClass after insert of the items
UPDATE `vn`.`itemTaxCountry` SET `taxClassFk` = 2
@ -896,7 +900,8 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric
(29, 4, 17, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()),
(30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, CURDATE()),
(31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, CURDATE()),
(32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE());
(32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, CURDATE()),
(33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, CURDATE());
INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`)
VALUES
@ -2350,7 +2355,7 @@ REPLACE INTO `vn`.`invoiceIn`(`id`, `serialNumber`,`serial`, `supplierFk`, `issu
INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`)
VALUES
(1, CURDATE(), 1, 237),
(1, CURDATE(), 1, 336.99),
(1, CURDATE(), 1, 15.25),
(2, CURDATE(), 1, 168),
(2, CURDATE(), 1, 55.17),
@ -2430,3 +2435,11 @@ CALL `cache`.`last_buy_refresh`(FALSE);
UPDATE `vn`.`item` SET `genericFk` = 9
WHERE `id` = 2;
INSERT INTO `bs`.`defaulter` (`clientFk`, `amount`, `created`, `defaulterSinced`)
VALUES
(1101, 500, CURDATE(), CURDATE()),
(1102, 500, CURDATE(), CURDATE()),
(1103, 500, CURDATE(), CURDATE()),
(1107, 500, CURDATE(), CURDATE()),
(1109, 500, CURDATE(), CURDATE());

File diff suppressed because it is too large Load Diff

View File

@ -304,6 +304,16 @@ export default {
saveNewInsuranceCredit: 'vn-client-credit-insurance-insurance-create button[type="submit"]',
anyCreditInsuranceLine: 'vn-client-credit-insurance-insurance-index vn-tbody > vn-tr',
},
clientDefaulter: {
anyClient: 'vn-client-defaulter-index vn-tbody > vn-tr',
firstClientName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(2) > span',
firstSalesPersonName: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(3) > span',
firstObservation: 'vn-client-defaulter-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > vn-textarea[ng-model="defaulter.observation"]',
allDefaulterCheckbox: 'vn-client-defaulter-index vn-thead vn-multi-check',
addObservationButton: 'vn-client-defaulter-index vn-button[icon="icon-notes"]',
observation: '.vn-dialog.shown vn-textarea[ng-model="$ctrl.defaulter.observation"]',
saveButton: 'button[response="accept"]'
},
clientContacts: {
addContactButton: 'vn-client-contact vn-icon[icon="add_circle"]',
name: 'vn-client-contact vn-textfield[ng-model="contact.name"]',
@ -526,6 +536,7 @@ export default {
acceptDialog: '.vn-dialog.shown button[response="accept"]',
acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]',
descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(4) > section > span',
descriptorDeliveryAgency: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(5) > section > span',
acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]',
acceptDeleteStowawayButton: '.vn-dialog.shown button[response="accept"]'
},
@ -603,10 +614,12 @@ export default {
ticketBasicData: {
agency: 'vn-autocomplete[ng-model="$ctrl.agencyModeId"]',
zone: 'vn-autocomplete[ng-model="$ctrl.zoneId"]',
shipped: 'vn-date-picker[ng-model="$ctrl.shipped"]',
nextStepButton: 'vn-step-control .buttons > section:last-child vn-button',
finalizeButton: 'vn-step-control .buttons > section:last-child button[type=submit]',
stepTwoTotalPriceDif: 'vn-ticket-basic-data-step-two > vn-side-menu div:nth-child(4)',
chargesReason: 'vn-ticket-basic-data-step-two div:nth-child(3) > vn-radio',
withoutNegatives: 'vn-check[ng-model="$ctrl.ticket.withoutNegatives"]',
},
ticketComponents: {
base: 'vn-ticket-components > vn-side-menu div:nth-child(1) > div:nth-child(2)'

View File

@ -0,0 +1,73 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Client defaulter path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('insurance', 'client');
await page.accessToSection('client.defaulter.index');
});
afterAll(async() => {
await browser.close();
});
it('should count the amount of clients in the turns section', async() => {
const result = await page.countElement(selectors.clientDefaulter.anyClient);
expect(result).toEqual(5);
});
it('should check contain expected client', async() => {
const clientName =
await page.waitToGetProperty(selectors.clientDefaulter.firstClientName, 'innerText');
const salesPersonName =
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
expect(clientName).toEqual('Ororo Munroe');
expect(salesPersonName).toEqual('salesPerson');
});
it('should first observation not changed', async() => {
const expectedObservation = 'Madness, as you know, is like gravity, all it takes is a little push';
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(result).toContain(expectedObservation);
});
it('should not add empty observation', async() => {
await page.waitToClick(selectors.clientDefaulter.allDefaulterCheckbox);
await page.waitToClick(selectors.clientDefaulter.addObservationButton);
await page.write(selectors.clientDefaulter.observation, '');
await page.waitToClick(selectors.clientDefaulter.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain(`The message can't be empty`);
});
it('shoul checked all defaulters', async() => {
await page.loginAndModule('insurance', 'client');
await page.accessToSection('client.defaulter.index');
await page.waitToClick(selectors.clientDefaulter.allDefaulterCheckbox);
});
it('should add observation for all clients', async() => {
await page.waitToClick(selectors.clientDefaulter.addObservationButton);
await page.write(selectors.clientDefaulter.observation, 'My new observation');
await page.waitToClick(selectors.clientDefaulter.saveButton);
});
it('should first observation changed', async() => {
const message = await page.waitForSnackbar();
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(message.text).toContain('Observation saved!');
expect(result).toContain('My new observation');
});
});

View File

@ -83,4 +83,62 @@ describe('Ticket Edit basic data path', () => {
await page.waitToClick(selectors.ticketBasicData.finalizeButton);
await page.waitForState('ticket.card.summary');
});
it(`should not find ticket`, async() => {
await page.doSearch('29');
const count = await page.countElement(selectors.ticketsIndex.searchResult);
expect(count).toEqual(0);
});
it(`should split ticket without negatives`, async() => {
const newAgency = 'Silla247';
const newDate = new Date();
newDate.setDate(newDate.getDate() - 1);
await page.accessToSearchResult('14');
await page.accessToSection('ticket.card.basicData.stepOne');
await page.autocompleteSearch(selectors.ticketBasicData.agency, newAgency);
await page.pickDate(selectors.ticketBasicData.shipped, newDate);
await page.waitToClick(selectors.ticketBasicData.nextStepButton);
await page.waitToClick(selectors.ticketBasicData.withoutNegatives);
await page.waitToClick(selectors.ticketBasicData.finalizeButton);
await page.waitForState('ticket.card.summary');
const newTicketAgency = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText');
const newTicketDate = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText');
expect(newAgency).toEqual(newTicketAgency);
expect(newTicketDate).toContain(newDate.getDate());
});
it(`should new ticket have sale of old ticket`, async() => {
await page.accessToSection('ticket.card.sale');
await page.waitForState('ticket.card.sale');
const item = await page.waitToGetProperty(selectors.ticketSales.firstSaleId, 'innerText');
expect(item).toEqual('4');
});
it(`should old ticket have old date and agency`, async() => {
const oldDate = new Date();
const oldAgency = 'Super-Man delivery';
await page.accessToSearchResult('14');
const oldTicketAgency = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText');
const oldTicketDate = await page
.waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText');
expect(oldTicketAgency).toEqual(oldAgency);
expect(oldTicketDate).toContain(oldDate.getDate());
});
});

View File

@ -9,7 +9,7 @@ describe('Entry import, create and edit buys path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'entry');
await page.accessToSearchResult('1');
await page.accessToSearchResult('3');
});
afterAll(async() => {
@ -19,7 +19,7 @@ describe('Entry import, create and edit buys path', () => {
it('should count the summary buys and find there only one at this point', async() => {
const buysCount = await page.countElement(selectors.entrySummary.anyBuyLine);
expect(buysCount).toEqual(1);
expect(buysCount).toEqual(2);
});
it('should navigate to the buy section and then click the import button opening the import form', async() => {
@ -41,11 +41,10 @@ describe('Entry import, create and edit buys path', () => {
await page.waitForTextInField(selectors.entryBuys.ref, '200573095, 200573106, 200573117, 200573506');
await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846');
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
const itemName = 'Melee Reinforced weapon heavy shield 1x0.5m';
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, itemName);
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m');
await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Container ammo box 1m');
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged weapon longbow 2m');
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Ranged weapon longbow 2m');
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Ranged weapon sniper rifle 300mm');
await page.autocompleteSearch(selectors.entryBuys.fourthImportedItem, 'Melee weapon heavy shield 1x0.5m');
await page.waitToClick(selectors.entryBuys.importBuysButton);
@ -57,7 +56,7 @@ describe('Entry import, create and edit buys path', () => {
});
it('should count the buys to find 4 buys have been added', async() => {
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 5);
await page.waitForNumberOfElements(selectors.entryBuys.anyBuyLine, 6);
});
it('should delete the four buys that were just added', async() => {

View File

@ -23,71 +23,63 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-isTooLittle:before {
content: "\e91b";
}
.icon-frozen:before {
content: "\e900";
}
.icon-Person:before {
content: "\e901";
}
.icon-handmadeArtificial:before {
content: "\e902";
}
.icon-fruit:before {
content: "\e903";
}
.icon-funeral:before {
content: "\e904";
}
.icon-treatments:before {
content: "\e905";
}
.icon-preserved:before {
content: "\e906";
}
.icon-greenery:before {
content: "\e907";
}
.icon-plant:before {
content: "\e908";
}
.icon-handmade:before {
content: "\e909";
.icon-100:before {
content: "\e95a";
}
.icon-accessory:before {
content: "\e90a";
}
.icon-artificial:before {
content: "\e90b";
.icon-account:before {
content: "\e92a";
}
.icon-flower:before {
content: "\e90c";
}
.icon-fixedPrice:before {
content: "\e90d";
.icon-actions:before {
content: "\e960";
}
.icon-addperson:before {
content: "\e90e";
}
.icon-supplierfalse:before {
content: "\e90f";
.icon-agency:before {
content: "\e938";
}
.icon-invoice-out:before {
content: "\e910";
.icon-albaran:before {
content: "\e94d";
}
.icon-invoice-in:before {
content: "\e911";
.icon-anonymous:before {
content: "\e930";
}
.icon-invoice-in-create:before {
content: "\e912";
.icon-apps:before {
content: "\e951";
}
.icon-artificial:before {
content: "\e90b";
}
.icon-attach:before {
content: "\e92e";
}
.icon-barcode:before {
content: "\e971";
}
.icon-basket:before {
content: "\e914";
}
.icon-basketadd:before {
content: "\e913";
}
.icon-basket:before {
content: "\e914";
.icon-bin:before {
content: "\e96f";
}
.icon-botanical:before {
content: "\e972";
}
.icon-bucket:before {
content: "\e97a";
}
.icon-buscaman:before {
content: "\e93b";
}
.icon-buyrequest:before {
content: "\e932";
}
.icon-calc_volum .path1:before {
content: "\e915";
@ -112,309 +104,294 @@
content: "\e91a";
margin-left: -1em;
}
.icon-deliveryprices:before {
content: "\e91c";
}
.icon-onlinepayment:before {
content: "\e91d";
}
.icon-risk:before {
content: "\e91e";
}
.icon-noweb:before {
content: "\e91f";
}
.icon-no036:before {
content: "\e920";
}
.icon-inactive:before {
content: "\e921";
}
.icon-unavailable:before {
content: "\e922";
}
.icon-invoice-01:before {
content: "\e923";
}
.icon-invoice:before {
content: "\e924";
}
.icon-supplier:before {
content: "\e925";
}
.icon-client2:before {
content: "\e926";
}
.icon-supplier2:before {
content: "\e927";
}
.icon-client:before {
content: "\e928";
}
.icon-shipment-01:before {
content: "\e929";
}
.icon-inventory:before {
content: "\e92b";
}
.icon-zone:before {
content: "\e92c";
}
.icon-wiki:before {
content: "\e92d";
}
.icon-attach:before {
content: "\e92e";
}
.icon-zone2:before {
content: "\e92f";
}
.icon-anonymous:before {
content: "\e930";
}
.icon-net:before {
content: "\e931";
}
.icon-buyrequest:before {
content: "\e932";
}
.icon-thermometer:before {
content: "\e933";
}
.icon-entry:before {
content: "\e934";
}
.icon-deletedTicket:before {
content: "\e935";
}
.icon-deliveryprices-01:before {
content: "\e936";
.icon-calendar:before {
content: "\e93d";
}
.icon-catalog:before {
content: "\e937";
}
.icon-agency:before {
content: "\e938";
}
.icon-delivery:before {
content: "\e939";
}
.icon-wand:before {
content: "\e93a";
}
.icon-buscaman:before {
content: "\e93b";
}
.icon-pbx:before {
content: "\e93c";
}
.icon-calendar:before {
content: "\e93d";
}
.icon-splitline:before {
content: "\e93e";
}
.icon-consignatarios:before {
content: "\e93f";
}
.icon-tax:before {
content: "\e940";
}
.icon-notes:before {
content: "\e941";
}
.icon-lines:before {
content: "\e942";
}
.icon-languaje:before {
content: "\e943";
}
.icon-greuge:before {
content: "\e944";
}
.icon-credit:before {
content: "\e945";
}
.icon-components:before {
content: "\e946";
}
.icon-pets:before {
content: "\e947";
}
.icon-linesprepaired:before {
content: "\e948";
}
.icon-control:before {
content: "\e949";
}
.icon-revision:before {
content: "\e94a";
}
.icon-newinvoices:before {
content: "\e94b";
}
.icon-services:before {
content: "\e94c";
}
.icon-newalbaran:before {
content: "\e94d";
}
.icon-solunion:before {
content: "\e94e";
}
.icon-stowaway:before {
content: "\e94f";
}
.icon-exit:before {
content: "\e950";
}
.icon-apps:before {
content: "\e951";
}
.icon-info:before {
content: "\e952";
}
.icon-columndelete:before {
content: "\e953";
}
.icon-columnadd:before {
content: "\e954";
}
.icon-deleteline:before {
content: "\e955";
}
.icon-item:before {
content: "\e956";
}
.icon-worker:before {
content: "\e957";
}
.icon-headercol:before {
content: "\e958";
}
.icon-reserva:before {
content: "\e959";
}
.icon-100:before {
content: "\e95a";
}
.icon-noweb1:before {
content: "\e95b";
}
.icon-settings1:before {
content: "\e95c";
}
.icon-sign:before {
content: "\e95d";
}
.icon-polizon:before {
content: "\e95e";
}
.icon-solclaim:before {
content: "\e95f";
}
.icon-actions:before {
content: "\e960";
}
.icon-details:before {
content: "\e961";
}
.icon-traceability:before {
content: "\e962";
}
.icon-claims:before {
content: "\e963";
}
.icon-regentry:before {
content: "\e964";
}
.icon-regentry-1:before {
content: "\e965";
}
.icon-transaction:before {
content: "\e966";
}
.icon-history:before {
content: "\e968";
}
.icon-entry:before {
content: "\e969";
}
.icon-mana:before {
content: "\e96a";
}
.icon-ticket:before {
content: "\e96b";
}
.icon-niche:before {
content: "\e96c";
}
.icon-tags:before {
content: "\e96d";
}
.icon-volume:before {
content: "\e96e";
}
.icon-bin:before {
content: "\e96f";
}
.icon-splur:before {
content: "\e970";
}
.icon-barcode:before {
content: "\e971";
}
.icon-botanical:before {
content: "\e972";
.icon-client:before {
content: "\e928";
}
.icon-clone:before {
content: "\e973";
}
.icon-photo:before {
content: "\e974";
.icon-columnadd:before {
content: "\e954";
}
.icon-sms:before {
content: "\e975";
.icon-columndelete:before {
content: "\e953";
}
.icon-eye:before {
content: "\e976";
.icon-components:before {
content: "\e946";
}
.icon-doc:before {
content: "\e977";
.icon-consignatarios:before {
content: "\e93f";
}
.icon-package:before {
content: "\e978";
.icon-control:before {
content: "\e949";
}
.icon-settings:before {
content: "\e979";
.icon-credit:before {
content: "\e927";
}
.icon-bucket:before {
content: "\e97a";
.icon-defaulter:before {
content: "\e94b";
}
.icon-mandatory:before {
content: "\e97b";
.icon-deletedTicket:before {
content: "\e935";
}
.icon-recovery:before {
content: "\e97c";
.icon-deleteline:before {
content: "\e955";
}
.icon-payment:before {
content: "\e97e";
.icon-delivery:before {
content: "\e939";
}
.icon-invoices:before {
content: "\e97f";
.icon-deliveryprices:before {
content: "\e91c";
}
.icon-grid:before {
content: "\e980";
}
.icon-logout:before {
content: "\e981";
}
.icon-web:before {
content: "\e982";
}
.icon-albaran:before {
content: "\e983";
.icon-details:before {
content: "\e961";
}
.icon-dfiscales:before {
content: "\e984";
}
.icon-disabled:before {
content: "\e921";
}
.icon-doc:before {
content: "\e977";
}
.icon-entry:before {
content: "\e934";
}
.icon-exit:before {
content: "\e92f";
}
.icon-eye:before {
content: "\e976";
}
.icon-fixedPrice:before {
content: "\e90d";
}
.icon-flower:before {
content: "\e90c";
}
.icon-frozen:before {
content: "\e900";
}
.icon-fruit:before {
content: "\e903";
}
.icon-funeral:before {
content: "\e904";
}
.icon-greenery:before {
content: "\e907";
}
.icon-greuge:before {
content: "\e944";
}
.icon-grid:before {
content: "\e980";
}
.icon-handmade:before {
content: "\e909";
}
.icon-handmadeArtificial:before {
content: "\e902";
}
.icon-headercol:before {
content: "\e958";
}
.icon-history:before {
content: "\e968";
}
.icon-info:before {
content: "\e952";
}
.icon-inventory:before {
content: "\e92b";
}
.icon-invoice:before {
content: "\e923";
}
.icon-invoice-in:before {
content: "\e911";
}
.icon-invoice-in-create:before {
content: "\e912";
}
.icon-invoice-out:before {
content: "\e910";
}
.icon-isTooLittle:before {
content: "\e91b";
}
.icon-item:before {
content: "\e956";
}
.icon-languaje:before {
content: "\e926";
}
.icon-lines:before {
content: "\e942";
}
.icon-linesprepaired:before {
content: "\e948";
}
.icon-logout:before {
content: "\e936";
}
.icon-mana:before {
content: "\e96a";
}
.icon-mandatory:before {
content: "\e97b";
}
.icon-net:before {
content: "\e931";
}
.icon-niche:before {
content: "\e96c";
}
.icon-no036:before {
content: "\e920";
}
.icon-noPayMethod:before {
content: "\e905";
}
.icon-notes:before {
content: "\e941";
}
.icon-noweb:before {
content: "\e91f";
}
.icon-onlinepayment:before {
content: "\e91d";
}
.icon-package:before {
content: "\e978";
}
.icon-payment:before {
content: "\e97e";
}
.icon-pbx:before {
content: "\e93c";
}
.icon-Person:before {
content: "\e901";
}
.icon-pets:before {
content: "\e947";
}
.icon-photo:before {
content: "\e924";
}
.icon-planta:before {
content: "\e908";
}
.icon-polizon:before {
content: "\e95e";
}
.icon-preserved:before {
content: "\e906";
}
.icon-recovery:before {
content: "\e97c";
}
.icon-regentry:before {
content: "\e964";
}
.icon-reserva:before {
content: "\e959";
}
.icon-revision:before {
content: "\e94a";
}
.icon-risk:before {
content: "\e91e";
}
.icon-services:before {
content: "\e94c";
}
.icon-settings:before {
content: "\e979";
}
.icon-shipment-01:before {
content: "\e929";
}
.icon-sign:before {
content: "\e95d";
}
.icon-sms:before {
content: "\e975";
}
.icon-solclaim:before {
content: "\e95f";
}
.icon-solunion:before {
content: "\e94e";
}
.icon-splitline:before {
content: "\e93e";
}
.icon-splur:before {
content: "\e970";
}
.icon-stowaway:before {
content: "\e94f";
}
.icon-supplier:before {
content: "\e925";
}
.icon-supplierfalse:before {
content: "\e90f";
}
.icon-tags:before {
content: "\e96d";
}
.icon-tax:before {
content: "\e940";
}
.icon-thermometer:before {
content: "\e933";
}
.icon-ticket:before {
content: "\e96b";
}
.icon-ticketAdd:before {
content: "\e945";
}
.icon-traceability:before {
content: "\e962";
}
.icon-transaction:before {
content: "\e966";
}
.icon-treatments:before {
content: "\e922";
}
.icon-unavailable:before {
content: "\e92c";
}
.icon-volume:before {
content: "\e96e";
}
.icon-wand:before {
content: "\e93a";
}
.icon-web:before {
content: "\e982";
}
.icon-wiki:before {
content: "\e92d";
}
.icon-worker:before {
content: "\e957";
}
.icon-zone:before {
content: "\e943";
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 158 KiB

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

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

View File

@ -291,7 +291,8 @@ module.exports = function(Self) {
* @param {*} properties Modified object properties
*/
function removeUnloggable(definition, properties) {
const propList = Object.keys(properties);
const objectCopy = Object.assign({}, properties);
const propList = Object.keys(objectCopy);
const propDefs = new Map();
for (let property in definition.properties) {
@ -302,10 +303,15 @@ module.exports = function(Self) {
for (let property of propList) {
const propertyDef = propDefs.get(property);
const firstChar = property.substring(0, 1);
const isPrivate = firstChar == '$';
if (!propertyDef) return;
if (isPrivate || !propertyDef)
delete properties[property];
if (propertyDef.log === false)
if (!propertyDef) continue;
if (propertyDef.log === false || isPrivate)
delete properties[property];
else if (propertyDef.logValue === false)
properties[property] = null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,90 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('filter', {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}
},
{
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by name`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return {or: [
{'d.clientFk': value},
{'d.clientName': {like: `%${value}%`}}
]};
}
});
filter = mergeFilters(ctx.args.filter, {where});
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT *
FROM (
SELECT
DISTINCT c.id clientFk,
c.name clientName,
c.salesPersonFk,
u.name salesPersonName,
d.amount,
co.created,
CONCAT(DATE(co.created), ' ', co.text) observation,
uw.id workerFk,
uw.name workerName,
c.creditInsurance,
d.defaulterSinced
FROM vn.defaulter d
JOIN vn.client c ON c.id = d.clientFk
LEFT JOIN vn.clientObservation co ON co.clientFk = c.id
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN account.user uw ON uw.id = co.workerFk
WHERE
d.created = CURDATE()
AND d.amount > 0
ORDER BY co.created DESC) d`
);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(`GROUP BY d.clientFk`);
stmt.merge(conn.makeOrderBy(filter.order));
const itemsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return itemsIndex === 0 ? result : result[itemsIndex];
};
};

View File

@ -0,0 +1,63 @@
const models = require('vn-loopback/server/server').models;
describe('defaulter filter()', () => {
const authUserId = 9;
it('should all return the tickets matching the filter', async() => {
const tx = await models.Defaulter.beginTransaction({});
try {
const options = {transaction: tx};
const filter = {};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {filter: filter}};
const result = await models.Defaulter.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.clientFk).toEqual(1101);
expect(result.length).toEqual(5);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the defaulter with id', async() => {
const tx = await models.Defaulter.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 1101}};
const result = await models.Defaulter.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.clientFk).toEqual(1101);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the defaulter matching the client name', async() => {
const tx = await models.Defaulter.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'bruce'}};
const result = await models.Defaulter.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.clientName).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -52,12 +52,13 @@ module.exports = Self => {
let response;
try {
if (process.env.NODE_ENV !== 'production')
params.fake = 1;
const jsonTest = {
json: params
};
response = await got.post(smsConfig.uri, jsonTest).json();
response = {result: [{status: 'ok'}]};
else {
const jsonTest = {
json: params
};
response = await got.post(smsConfig.uri, jsonTest).json();
}
} catch (e) {
console.error(e);
}

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/defaulter/filter')(Self);
};

View File

@ -8,6 +8,9 @@
}
},
"properties": {
"id": {
"type": "Number"
},
"created": {
"type": "Date"
},

View File

@ -0,0 +1,186 @@
<vn-crud-model
vn-id="model"
url="Defaulters/filter"
filter="::$ctrl.filter"
limit="20"
data="defaulters"
auto-load="true">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
placeholder="Search client"
info="Search client by id or name"
auto-state="false"
model="model">
</vn-searchbar>
</vn-portal>
<vn-data-viewer
model="model"
class="vn-w-xl">
<vn-card>
<vn-tool-bar>
<div class="vn-pa-md">
<div class="totalBox" style="text-align: center;">
<h6 translate>Total</h6>
<vn-label-value
label="Balance due"
value="{{$ctrl.balanceDueTotal}} €">
</vn-label-value>
</div>
</div>
<div class="vn-pa-md">
<vn-button
ng-show="$ctrl.checked.length > 0"
ng-click="notesDialog.show()"
name="notesDialog"
vn-tooltip="Add observation"
icon="icon-notes">
</vn-button>
</div>
</vn-tool-bar>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="clientName">Client</vn-th>
<vn-th field="salesPersonFk">Comercial</vn-th>
<vn-th
field="amount"
vn-tooltip="Balance due"
number>
Balance D.
</vn-th>
<vn-th
vn-tooltip="Worker who made the last observation"
shrink>
Author
</vn-th>
<vn-th expand>Last observation</vn-th>
<vn-th
vn-tooltip="Credit insurance"
number>
Credit I.
</vn-th>
<vn-th shrink-datetime>From</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="defaulter in defaulters">
<vn-td shrink>
<vn-check
ng-model="defaulter.checked"
vn-click-stop>
</vn-check>
</vn-td>
<vn-td>
<span
vn-click-stop="clientDescriptor.show($event, defaulter.clientFk)"
title ="{{::defaulter.clientName}}"
class="link">
{{::defaulter.clientName}}
</span>
</vn-td>
<vn-td>
<span
title="{{::defaulter.salesPersonName}}"
vn-click-stop="workerDescriptor.show($event, defaulter.salesPersonFk)"
class="link" >
{{::defaulter.salesPersonName | dashIfEmpty}}
</span>
</vn-td>
<vn-td number>{{::defaulter.amount}}</vn-td>
<vn-td shrink>
<span
title="{{::defaulter.workerName}}"
vn-click-stop="workerDescriptor.show($event, defaulter.workerFk)"
class="link" >
{{::defaulter.workerName | dashIfEmpty}}
</span>
</vn-td>
<vn-td expand>
<vn-textarea
vn-three
disabled="true"
label="Observation"
ng-model="defaulter.observation">
</vn-textarea>
</vn-td>
<vn-td number>{{::defaulter.creditInsurance}}</vn-td>
<vn-td shrink-datetime>{{::defaulter.defaulterSinced | date: 'dd/MM/yyyy'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-popup vn-id="dialog-summary-client">
<vn-client-summary
client="$ctrl.clientSelected">
</vn-client-summary>
</vn-popup>
<!--Context menu-->
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>
<!-- Dialog of add notes button -->
<vn-dialog
vn-id="notesDialog"
on-accept="$ctrl.onResponse()">
<tpl-body>
<section class="SMSDialog">
<h5 class="vn-py-sm">{{$ctrl.$t('Add observation to all selected clients', {total: $ctrl.checked.length})}}</h5>
<vn-horizontal>
<vn-textarea vn-one
vn-id="message"
label="Message"
ng-model="$ctrl.defaulter.observation"
rows="3"
required="true"
rule>
</vn-textarea>
</vn-horizontal>
</section>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,65 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.defaulter = {};
}
get balanceDueTotal() {
let balanceDueTotal = 0;
if (this.checked.length > 0) {
for (let defaulter of this.checked)
balanceDueTotal += defaulter.amount;
return balanceDueTotal;
}
return balanceDueTotal;
}
get checked() {
const clients = this.$.model.data || [];
const checkedLines = [];
for (let defaulter of clients) {
if (defaulter.checked)
checkedLines.push(defaulter);
}
return checkedLines;
}
onResponse() {
if (!this.defaulter.observation)
throw new UserError(`The message can't be empty`);
const params = [];
for (let defaulter of this.checked) {
params.push({
text: this.defaulter.observation,
clientFk: defaulter.clientFk
});
}
this.$http.post(`ClientObservations`, params) .then(() => {
this.vnApp.showMessage(this.$t('Observation saved!'));
this.$state.reload();
});
}
exprBuilder(param, value) {
switch (param) {
case 'clientName':
case 'salesPersonFk':
return {[`d.${param}`]: value};
}
}
}
ngModule.vnComponent('vnClientDefaulterIndex', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,98 @@
import './index';
import crudModel from 'core/mocks/crud-model';
describe('client defaulter', () => {
describe('Component vnClientDefaulterIndex', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('client'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-client-defaulter></vn-client-defaulter>');
controller = $componentController('vnClientDefaulterIndex', {$element});
controller.$.model = crudModel;
controller.$.model.data = [
{clientFk: 1101, amount: 125},
{clientFk: 1102, amount: 500},
{clientFk: 1103, amount: 250}
];
}));
describe('checked() getter', () => {
it('should return the checked lines', () => {
const data = controller.$.model.data;
data[1].checked = true;
data[2].checked = true;
const checkedRows = controller.checked;
const firstCheckedRow = checkedRows[0];
const secondCheckedRow = checkedRows[1];
expect(firstCheckedRow.clientFk).toEqual(1102);
expect(secondCheckedRow.clientFk).toEqual(1103);
});
});
describe('balanceDueTotal() getter', () => {
it('should return balance due total', () => {
const data = controller.$.model.data;
data[1].checked = true;
data[2].checked = true;
const checkedRows = controller.checked;
const expectedAmount = checkedRows[0].amount + checkedRows[1].amount;
const result = controller.balanceDueTotal;
expect(result).toEqual(expectedAmount);
});
});
describe('onResponse()', () => {
it('should return error for empty message', () => {
let error;
try {
controller.onResponse();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`The message can't be empty`);
});
it('should return saved message', () => {
const data = controller.$.model.data;
data[1].checked = true;
controller.defaulter = {observation: 'My new observation'};
const params = [{text: controller.defaulter.observation, clientFk: data[1].clientFk}];
jest.spyOn(controller.vnApp, 'showMessage');
$httpBackend.expect('POST', `ClientObservations`, params).respond(200, params);
controller.onResponse();
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Observation saved!');
});
});
describe('exprBuilder()', () => {
it('should search by sales person', () => {
let expr = controller.exprBuilder('salesPersonFk', '5');
expect(expr).toEqual({'d.salesPersonFk': '5'});
});
it('should search by client name', () => {
let expr = controller.exprBuilder('clientName', '1foo');
expect(expr).toEqual({'d.clientName': '1foo'});
});
});
});
});

View File

@ -0,0 +1,7 @@
Last observation: Última observación
Add observation: Añadir observación
Search client: Buscar clientes
Add observation to all selected clients: Añadir observación a {{total}} cliente(s) seleccionado(s)
Credit I.: Crédito A.
Balance D.: Saldo V.
Worker who made the last observation: Trabajador que ha realizado la última observación

View File

@ -44,3 +44,4 @@ import './dms/create';
import './dms/edit';
import './consumption';
import './consumption-search-panel';
import './defaulter';

View File

@ -33,6 +33,7 @@ Search client by id or name: Buscar clientes por identificador o nombre
# Sections
Clients: Clientes
Defaulter: Morosos
New client: Nuevo cliente
Fiscal data: Datos fiscales
Billing data: Forma de pago

View File

@ -6,7 +6,8 @@
"dependencies": ["worker", "invoiceOut"],
"menus": {
"main": [
{"state": "client.index", "icon": "person"}
{"state": "client.index", "icon": "person"},
{"state": "client.defaulter.index", "icon": "icon-defaulter"}
],
"card": [
{"state": "client.card.basicData", "icon": "settings"},
@ -360,6 +361,18 @@
"params": {
"client": "$ctrl.client"
}
},
{
"url": "/defaulter",
"state": "client.defaulter",
"component": "ui-view",
"description": "Defaulter"
},
{
"url": "/index?q",
"state": "client.defaulter.index",
"component": "vn-client-defaulter-index",
"description": "Defaulter"
}
]
}

View File

@ -52,23 +52,59 @@ module.exports = Self => {
}
try {
const entry = await models.Entry.findById(id, null, myOptions);
const entry = await models.Entry.findById(id, {
include: [{
relation: 'travel',
scope: {
fields: ['warehouseInFk', 'landed'],
}
}]
}, myOptions);
await entry.updateAttributes({
observation: args.observation,
ref: args.ref
}, myOptions);
const travel = entry.travel();
await Self.rawSql('CALL buyUltimate(?, ?)', [
travel.warehouseInFk,
travel.landed
], myOptions);
const buyUltimate = await Self.rawSql(`
SELECT *
FROM tmp.buyUltimate
WHERE warehouseFk = ?
`, [travel.warehouseInFk], myOptions);
const lastItemBuys = new Map();
for (const buy of buyUltimate)
lastItemBuys.set(buy.itemFk, buy);
const buys = [];
for (let buy of args.buys) {
if (!buy.itemFk)
throw new Error('The item is required');
const lastItemBuy = lastItemBuys.get(buy.itemFk);
if (!lastItemBuy) continue;
const lastBuy = await models.Buy.findById(lastItemBuy.buyFk, null, myOptions);
const stickers = 1;
const quantity = (stickers * buy.packing);
buys.push({
entryFk: entry.id,
itemFk: buy.itemFk,
stickers: 1,
quantity: 1,
stickers: stickers,
quantity: quantity,
packing: buy.packing,
grouping: buy.grouping,
buyingValue: buy.buyingValue,
packageFk: buy.packageFk
packageFk: buy.packageFk,
groupingMode: lastBuy.groupingMode,
weight: lastBuy.weight
});
await models.ItemMatchProperties.upsert({
@ -79,6 +115,8 @@ module.exports = Self => {
}, myOptions);
}
if (!buys.length) return;
const createdBuys = await models.Buy.create(buys, myOptions);
const buyIds = createdBuys.map(buy => buy.id);

View File

@ -0,0 +1,91 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('lastItemBuys', {
description: 'Returns a list of items from last buys',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'origin itemId',
http: {source: 'path'}
},
{
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/lastItemBuys`,
verb: 'GET'
}
});
Self.lastItemBuys = async(id, filter, options) => {
const conn = Self.dataSource.connector;
const models = Self.app.models;
const where = {isActive: true};
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
filter = mergeFilters(filter, {where});
const entry = await models.Entry.findById(id, {
include: [{
relation: 'travel',
scope: {
fields: ['warehouseInFk', 'landed'],
}
}]
}, myOptions);
const travel = entry.travel();
const stmts = [];
let stmt;
stmt = new ParameterizedSQL(`CALL buyUltimate(?, ?)`, [
travel.warehouseInFk,
travel.landed
]);
stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item');
stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.item
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT
i.*,
p.name AS producerName,
nk.name AS inkName
FROM item i
JOIN producer p ON p.id = i.producerFk
JOIN ink nk ON nk.id = i.inkFk
JOIN tmp.buyUltimate bu ON i.id = bu.itemFk
AND bu.warehouseFk = ?
`, [travel.warehouseInFk]);
stmts.push(stmt);
stmt = new ParameterizedSQL('SELECT * FROM tmp.item');
stmt.merge(conn.makeSuffix(filter));
const itemsIndex = stmts.push(stmt) - 1;
stmts.push('DROP TEMPORARY TABLE tmp.item');
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return result[itemsIndex];
};
};

View File

@ -3,9 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('entry import()', () => {
const buyerId = 35;
const companyId = 442;
const travelId = 1;
const supplierId = 1;
const entryId = 3;
const activeCtx = {
accessToken: {userId: buyerId},
};
@ -51,25 +49,17 @@ describe('entry import()', () => {
const tx = await models.Entry.beginTransaction({});
try {
const options = {transaction: tx};
const newEntry = await models.Entry.create({
dated: new Date(),
supplierFk: supplierId,
travelFk: travelId,
companyFk: companyId,
observation: 'The entry',
ref: 'Entry ref'
}, options);
await models.Entry.importBuys(ctx, newEntry.id, options);
await models.Entry.importBuys(ctx, entryId, options);
const updatedEntry = await models.Entry.findById(newEntry.id, null, options);
const updatedEntry = await models.Entry.findById(entryId, null, options);
const entryBuys = await models.Buy.find({
where: {entryFk: newEntry.id}
where: {entryFk: entryId}
}, options);
expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef);
expect(entryBuys.length).toEqual(2);
expect(entryBuys.length).toEqual(4);
await tx.rollback();
} catch (e) {

View File

@ -0,0 +1,26 @@
const models = require('vn-loopback/server/server').models;
describe('Entry lastItemBuys()', () => {
const entryId = 3;
it('should return the items that have been bought', async() => {
const filter = {where: {}};
const result = await models.Entry.lastItemBuys(entryId, filter);
expect(result.length).toBeGreaterThan(1);
});
it('should return the items matching color "brown"', async() => {
const brownInkCode = 'BRW';
const filter = {where: {inkFk: brownInkCode}};
const result = await models.Entry.lastItemBuys(entryId, filter);
expect(result.length).toEqual(2);
});
it('should return the items with a name containing "Ranged"', async() => {
const filter = {where: {name: {like: '%Ranged%'}}};
const result = await models.Entry.lastItemBuys(entryId, filter);
expect(result.length).toEqual(3);
});
});

View File

@ -5,4 +5,5 @@ module.exports = Self => {
require('../methods/entry/addBuy')(Self);
require('../methods/entry/importBuys')(Self);
require('../methods/entry/importBuysPreview')(Self);
require('../methods/entry/lastItemBuys')(Self);
};

View File

@ -35,6 +35,13 @@
{{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) &#x2192;
{{::warehouseOutName}} ({{::landed | date: 'dd/MM/yyyy'}})
</tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.entry.travelFk)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
@ -122,3 +129,93 @@
</vn-button>
</vn-button-bar>
</form>
<!-- Filter travel dialog -->
<vn-dialog
vn-id="filterDialog"
message="Filter travel">
<tpl-body class="travelFilter">
<vn-horizontal>
<vn-autocomplete
label="Agency"
ng-model="$ctrl.travelFilterParams.agencyFk"
url="AgencyModes"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse Out"
ng-model="$ctrl.travelFilterParams.warehouseOutFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse In"
ng-model="$ctrl.travelFilterParams.warehouseInFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-date-picker
label="Shipped"
ng-model="$ctrl.travelFilterParams.shipped">
</vn-date-picker>
<vn-date-picker
label="Landed"
ng-model="$ctrl.travelFilterParams.landed">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mb-md">
<vn-button vn-none
label="Search"
ng-click="$ctrl.filter()">
</vn-button>
</vn-horizontal>
<vn-crud-model
vn-id="travelsModel"
url="Travels"
filter="$ctrl.travelFilter"
data="travels"
limit="10">
</vn-crud-model>
<vn-data-viewer
model="travelsModel"
class="vn-w-lg">
<vn-table class="scrollable">
<vn-thead>
<vn-tr>
<vn-th shrink>ID</vn-th>
<vn-th expand>Agency</vn-th>
<vn-th expand>Warehouse Out</vn-th>
<vn-th expand>Warehouse In</vn-th>
<vn-th expand>Shipped</vn-th>
<vn-th expand>Landed</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="travel in travels"
class="clickable vn-tr search-result"
ng-click="$ctrl.selectTravel(travel.id)">
<vn-td shrink>
<span
vn-click-stop="travelDescriptor.show($event, travel.id)"
class="link">
{{::travel.id}}
</span>
</vn-td>
<vn-td expand>{{::travel.agency.name}}</vn-td>
<vn-td expand>{{::travel.warehouseOut.name}}</vn-td>
<vn-td expand>{{::travel.warehouseIn.name}}</vn-td>
<vn-td expand>{{::travel.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td expand>{{::travel.landed | date: 'dd/MM/yyyy'}}</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-travel-descriptor-popover
vn-id="travel-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-travel-descriptor-popover>
</tpl-body>
</vn-dialog>

View File

@ -1,10 +1,68 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
showFilterDialog(travel) {
this.activeTravel = travel;
this.travelFilterParams = {};
this.travelFilter = {
include: [
{
relation: 'agency',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseIn',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseOut',
scope: {
fields: ['name']
}
}
]
};
this.$.filterDialog.show();
}
selectTravel(id) {
this.entry.travelFk = id;
this.$.filterDialog.hide();
}
filter() {
const filter = this.travelFilter;
const params = this.travelFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
switch (key) {
case 'agencyFk':
case 'warehouseInFk':
case 'warehouseOutFk':
case 'shipped':
case 'landed':
where[key] = value;
}
}
filter.where = where;
this.$.travelsModel.applyFilter(filter);
}
}
ngModule.vnComponent('vnEntryBasicData', {
template: require('./index.html'),
controller: Section,
bindings: {
entry: '<'
}
},
controller: Controller
});

View File

@ -0,0 +1,3 @@
.travelFilter{
width: 950px;
}

View File

@ -59,7 +59,7 @@
<vn-autocomplete
class="dense"
vn-focus
url="Items/withName"
url="Entries/{{$ctrl.$params.id}}/lastItemBuys"
ng-model="buy.itemFk"
show-field="name"
value-field="id"
@ -119,15 +119,18 @@
<vn-textfield
label="Name"
ng-model="$ctrl.itemFilterParams.name"
ng-keydown="$ctrl.onKeyPress($event)"
vn-focus>
</vn-textfield>
<vn-textfield
label="Size"
ng-model="$ctrl.itemFilterParams.size">
ng-model="$ctrl.itemFilterParams.size"
ng-keydown="$ctrl.onKeyPress($event)">
</vn-textfield>
<vn-autocomplete
label="Producer"
ng-model="$ctrl.itemFilterParams.producerFk"
ng-keydown="$ctrl.onKeyPress($event)"
url="Producers"
show-field="name"
value-field="id">
@ -135,6 +138,7 @@
<vn-autocomplete
label="Type"
ng-model="$ctrl.itemFilterParams.typeFk"
ng-keydown="$ctrl.onKeyPress($event)"
url="ItemTypes"
show-field="name"
value-field="id">
@ -142,6 +146,7 @@
<vn-autocomplete
label="Color"
ng-model="$ctrl.itemFilterParams.inkFk"
ng-keydown="$ctrl.onKeyPress($event)"
url="Inks"
show-field="name"
value-field="id">
@ -155,7 +160,7 @@
</vn-horizontal>
<vn-crud-model
vn-id="itemsModel"
url="Items/withName"
url="Entries/{{$ctrl.$params.id}}/lastItemBuys"
filter="$ctrl.itemFilter"
data="items"
limit="10">
@ -186,8 +191,8 @@
</vn-td>
<vn-td expand>{{::item.name}}</vn-td>
<vn-td number>{{::item.size}}</vn-td>
<vn-td expand>{{::item.producer.name}}</vn-td>
<vn-td>{{::item.ink.name}}</vn-td>
<vn-td expand>{{::item.producerName}}</vn-td>
<vn-td>{{::item.inkName}}</vn-td>
</a>
</vn-tbody>
</vn-table>

View File

@ -141,6 +141,11 @@ class Controller extends Section {
filter.where = where;
this.$.itemsModel.applyFilter(filter);
}
onKeyPress($event) {
if ($event.key === 'Enter')
this.filter();
}
}
Controller.$inject = ['$element', '$scope'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,20 @@
ng-model="filter.fi">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one ng-model="filter.supplierFk"
url="Suppliers"
show-field="nickname"
search-function="{or: [{id: $search}, {nickname: {like: '%'+ $search +'%'}}]}"
value-field="id"
order="nickname"
label="Supplier">
<tpl-item>
{{::id}} - {{::nickname}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const got = require('got');
const path = require('path');
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('createPdf', {
@ -57,39 +57,37 @@ module.exports = Self => {
hasPdf: true
}, myOptions);
const response = got.stream(`${origin}/api/report/invoice`, {
searchParams: {
return axios.get(`${origin}/api/report/invoice`, {
responseType: 'stream',
params: {
authorization: auth.id,
invoiceId: id
}
});
}).then(async response => {
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 container = await models.InvoiceContainer.container(year);
const rootPath = container.client.root;
const fileName = `${year}${invoiceOut.ref}.pdf`;
const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
const container = await models.InvoiceContainer.container(year);
const rootPath = container.client.root;
const fileName = `${year}${invoiceOut.ref}.pdf`;
const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
await fs.mkdir(src, {recursive: true});
await fs.mkdir(src, {recursive: true});
if (tx) await tx.commit();
if (tx) await tx.commit();
response.data.pipe(fs.createWriteStream(fileSrc));
}).catch(async e => {
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => response.pipe(writeStream));
writeStream.on('finish', () => writeStream.end());
return new Promise(resolve => {
writeStream.on('close', () => resolve(invoiceOut));
throw e;
});
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
throw e;
}
};

View File

@ -1,24 +1,28 @@
const models = require('vn-loopback/server/server').models;
const got = require('got');
const LoopBackContext = require('loopback-context');
const fs = require('fs-extra');
const axios = require('axios');
describe('InvoiceOut createPdf()', () => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
const activeCtx = {
accessToken: {userId: userId, id: 'DEFAULT_TOKEN'},
headers: {origin: 'http://localhost:5000'}
};
const ctx = {req: activeCtx};
it('should create a new PDF file and set true the hasPdf property', async() => {
const invoiceId = 1;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const response = {
pipe: () => {},
on: () => {},
data: {
pipe: () => {},
on: () => {},
}
};
spyOn(got, 'stream').and.returnValue(response);
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(models.InvoiceContainer, 'container').and.returnValue({
client: {root: '/path'}
});
@ -32,9 +36,10 @@ describe('InvoiceOut createPdf()', () => {
const options = {transaction: tx};
try {
const result = await models.InvoiceOut.createPdf(ctx, invoiceId, options);
await models.InvoiceOut.createPdf(ctx, invoiceId, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceId, null, options);
expect(result.hasPdf).toBe(true);
expect(invoiceOut.hasPdf).toBe(true);
await tx.rollback();
} catch (e) {

View File

@ -19,6 +19,10 @@ class Controller extends Section {
this.id = value.id;
}
get hasInvoicing() {
return this.aclService.hasAny(['invoicing']);
}
loadData() {
const filter = {
include: [
@ -51,8 +55,13 @@ class Controller extends Section {
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.$state.reload())
.then(() => {
const isInsideInvoiceOut = this.$state.current.name.startsWith('invoiceOut');
if (isInsideInvoiceOut)
this.$state.go('invoiceOut.index');
else
this.$state.reload();
})
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}

View File

@ -50,6 +50,35 @@ describe('vnInvoiceOutDescriptorMenu', () => {
});
});
describe('deleteInvoiceOut()', () => {
it(`should make a query and call showSuccess()`, () => {
controller.invoiceOut = invoiceOut;
controller.$state.reload = jest.fn();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't in invoiceOut module`, () => {
controller.invoiceOut = invoiceOut;
jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'invoiceOut.card.something';
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('invoiceOut.index');
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');

View File

@ -11,7 +11,7 @@ vn-item-waste-detail {
padding-bottom: 7px;
padding-bottom: 4px;
font-weight: lighter;
background-color: #fde6ca;
background-color: $color-bg;
border-bottom: 1px solid #f7931e;
white-space: nowrap;
overflow: hidden;

View File

@ -133,6 +133,10 @@ module.exports = Self => {
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': {inq: value}}
: {'t.nickname': {like: `%${value}%`}};
case 'from':
return {'t.shipped': {gte: value}};
case 'to':

View File

@ -28,3 +28,4 @@ Real hour: Hora real
T. Hour: Hora T.
Theoretical hour: Hora Teórica
Go to the order: Ir al pedido
Basket: Cesta

View File

@ -7,7 +7,7 @@
<vn-icon-button icon="launch"></vn-icon-button>
</a>
<span>
Ticket #{{$ctrl.summary.id}} - {{$ctrl.summary.client.name}}
<span translate>Basket</span> #{{$ctrl.summary.id}} - {{$ctrl.summary.client.name}}
({{$ctrl.summary.client.salesPersonFk}})
</span>
<vn-button

View File

@ -77,6 +77,12 @@ module.exports = Self => {
type: 'number',
description: 'Action id',
required: true
},
{
arg: 'isWithoutNegatives',
type: 'boolean',
description: 'Is whithout negatives',
required: true
}],
returns: {
type: ['object'],
@ -127,6 +133,19 @@ module.exports = Self => {
}
}
if (args.isWithoutNegatives) {
const query = `CALL ticket_getMovable(?,?,?)`;
const params = [args.id, args.shipped, args.warehouseFk];
const [salesMovable] = await Self.rawSql(query, params, myOptions);
const salesNewTicket = salesMovable.filter(sale => (sale.movable ? sale.movable : 0) >= sale.quantity);
if (salesNewTicket.length) {
const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions);
args.id = newTicket.id;
}
}
const originalTicket = await models.Ticket.findOne({
where: {id: args.id},
fields: [
@ -230,6 +249,7 @@ module.exports = Self => {
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
}
res.id = args.id;
if (tx) await tx.commit();
return res;

View File

@ -9,10 +9,14 @@ module.exports = Self => {
description: 'ticket id',
http: {source: 'path'}
}],
returns: {
type: ['Object'],
root: true
returns: [{
arg: 'saleVolume',
type: ['object']
},
{
arg: 'packingTypeVolume',
type: ['object']
}],
http: {
path: `/:id/getVolume`,
verb: 'GET'
@ -25,7 +29,21 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
return Self.rawSql(`SELECT * FROM vn.saleVolume
WHERE ticketFk = ?`, [ticketFk], myOptions);
const saleVolume = await Self.rawSql(`
SELECT saleFk, volume
FROM vn.saleVolume
WHERE ticketFk = ?`, [ticketFk], myOptions);
const packingTypeVolume = await Self.rawSql(`
SELECT s.itemPackingTypeFk code,
i.description,
SUM(s.volume) volume
FROM vn.saleVolume s
LEFT JOIN vn.itemPackingType i
ON i.code = s.itemPackingTypeFk
WHERE s.ticketFk = ?
GROUP BY s.itemPackingTypeFk`, [ticketFk], myOptions);
return [saleVolume, packingTypeVolume];
};
};

View File

@ -40,6 +40,12 @@ module.exports = Self => {
type: 'number',
description: 'The warehouse id',
required: true
},
{
arg: 'shipped',
type: 'date',
description: 'shipped',
required: true
}],
returns: {
type: ['object'],
@ -104,19 +110,34 @@ module.exports = Self => {
totalDifference: 0.00,
};
const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`;
const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId];
// Get items movable
const ticketOrigin = await models.Ticket.findById(args.id, null, myOptions);
const differenceShipped = ticketOrigin.shipped.getTime() > args.shipped.getTime();
const differenceWarehouse = ticketOrigin.warehouseFk != args.warehouseId;
salesObj.haveDifferences = differenceShipped || differenceWarehouse;
let query = `CALL ticket_getMovable(?,?,?)`;
let params = [args.id, args.shipped, args.warehouseId];
const [salesMovable] = await Self.rawSql(query, params, myOptions);
const itemMovable = new Map();
for (sale of salesMovable) {
const saleMovable = sale.movable ? sale.movable : 0;
itemMovable.set(sale.id, saleMovable);
}
// Sale price component, one per sale
query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`;
params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId];
const [difComponents] = await Self.rawSql(query, params, myOptions);
const map = new Map();
// Sale price component, one per sale
for (difComponent of difComponents)
map.set(difComponent.saleFk, difComponent);
for (sale of salesObj.items) {
const difComponent = map.get(sale.id);
if (difComponent) {
sale.component = difComponent;
@ -129,6 +150,7 @@ module.exports = Self => {
salesObj.totalUnitPrice += sale.price;
salesObj.totalUnitPrice = round(salesObj.totalUnitPrice);
sale.movable = itemMovable.get(sale.id);
}
if (tx) await tx.commit();

View File

@ -45,7 +45,8 @@ describe('ticket componentUpdate()', () => {
shipped: today,
landed: tomorrow,
isDeleted: false,
option: 1
option: 1,
isWithoutNegatives: false
};
let ctx = {
@ -94,7 +95,8 @@ describe('ticket componentUpdate()', () => {
shipped: today,
landed: tomorrow,
isDeleted: false,
option: 1
option: 1,
isWithoutNegatives: false
};
const ctx = {
@ -134,4 +136,60 @@ describe('ticket componentUpdate()', () => {
throw e;
}
});
it('should change warehouse and without negatives', async() => {
const tx = await models.SaleComponent.beginTransaction({});
try {
const options = {transaction: tx};
const saleToTransfer = 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: true
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
await models.Ticket.componentUpdate(ctx, options);
const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options);
const oldTicket = await models.Ticket.findById(ticketID, null, options);
const newTicket = await models.Ticket.findById(newTicketID.id, null, options);
const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options);
expect(oldTicket.shipped).toEqual(originDate);
expect(newTicket.shipped).toEqual(newDate);
expect(newTicketSale.id).toEqual(saleToTransfer);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -8,9 +8,15 @@ describe('ticket getVolume()', () => {
const options = {transaction: tx};
const ticketId = 1;
const result = await models.Ticket.getVolume(ticketId, options);
const expectedSaleVolume = 1.09;
const expectedPackingTypeVolume = 0.028;
expect(result[0].volume).toEqual(1.09);
const result = await models.Ticket.getVolume(ticketId, options);
const [saleVolume] = result[0];
const [packingTypeVolume] = result[1];
expect(saleVolume.volume).toEqual(expectedSaleVolume);
expect(packingTypeVolume.volume).toEqual(expectedPackingTypeVolume);
await tx.rollback();
} catch (e) {

View File

@ -15,6 +15,7 @@ describe('sale priceDifference()', () => {
ctx.args = {
id: 16,
landed: tomorrow,
shipped: tomorrow,
addressId: 126,
agencyModeId: 7,
zoneId: 3,
@ -45,6 +46,7 @@ describe('sale priceDifference()', () => {
ctx.args = {
id: 1,
landed: new Date(),
shipped: new Date(),
addressId: 121,
zoneId: 3,
warehouseId: 1
@ -59,4 +61,38 @@ describe('sale priceDifference()', () => {
expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`));
});
it('should return ticket movable', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const ctx = {req: {accessToken: {userId: 1106}}};
ctx.args = {
id: 11,
shipped: tomorrow,
landed: tomorrow,
addressId: 122,
agencyModeId: 7,
zoneId: 3,
warehouseId: 1
};
const result = await models.Ticket.priceDifference(ctx, options);
const firstItem = result.items[0];
const secondtItem = result.items[1];
expect(firstItem.movable).toEqual(440);
expect(secondtItem.movable).toEqual(1980);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -201,7 +201,8 @@ class Controller extends Component {
addressId: this.ticket.addressFk,
agencyModeId: this.ticket.agencyModeFk,
zoneId: this.ticket.zoneFk,
warehouseId: this.ticket.warehouseFk
warehouseId: this.ticket.warehouseFk,
shipped: this.ticket.shipped
};
return this.$http.post(query, params).then(res => {

View File

@ -9,6 +9,7 @@
<vn-tr>
<vn-th number>Item</vn-th>
<vn-th class="align-center">Description</vn-th>
<vn-th ng-if="$ctrl.ticket.sale.haveDifferences" number>Movable</vn-th>
<vn-th number>Quantity</vn-th>
<vn-th number>Price (PPU)</vn-th>
<vn-th number>New (PPU)</vn-th>
@ -31,6 +32,13 @@
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td ng-if="$ctrl.ticket.sale.haveDifferences" number>
<span
class="chip"
ng-class="{'alert': sale.quantity>sale.movable}">
{{::sale.movable}}
</span>
</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.price | currency: 'EUR': 2}}</vn-td>
<vn-td number>{{::sale.component.newPrice | currency: 'EUR': 2}}</vn-td>
@ -66,6 +74,13 @@
</div>
</div>
</vn-card>
<div class="totalBox align-left" ng-show="::$ctrl.haveNegatives">
<vn-check
ng-model="$ctrl.ticket.withoutNegatives"
label="Create without negatives"
info="Clone this ticket with the changes and only sales availables">
</vn-check>
</div>
</div>
</vn-side-menu>

View File

@ -20,6 +20,7 @@ class Controller extends Component {
this.getTotalNewPrice();
this.getTotalDifferenceOfPrice();
this.loadDefaultTicketAction();
this.ticketHaveNegatives();
}
loadDefaultTicketAction() {
@ -63,6 +64,22 @@ class Controller extends Component {
this.totalPriceDifference = totalPriceDifference;
}
ticketHaveNegatives() {
let haveNegatives = false;
let haveNotNegatives = false;
const haveDifferences = this.ticket.sale.haveDifferences;
this.ticket.sale.items.forEach(item => {
if (item.quantity > item.movable)
haveNegatives = true;
else
haveNotNegatives = true;
});
this.ticket.withoutNegatives = false;
this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences);
}
onSubmit() {
if (!this.ticket.option) {
return this.vnApp.showError(
@ -70,8 +87,8 @@ class Controller extends Component {
);
}
let query = `tickets/${this.ticket.id}/componentUpdate`;
let params = {
const query = `tickets/${this.ticket.id}/componentUpdate`;
const params = {
clientFk: this.ticket.clientFk,
nickname: this.ticket.nickname,
agencyModeFk: this.ticket.agencyModeFk,
@ -82,16 +99,20 @@ class Controller extends Component {
shipped: this.ticket.shipped,
landed: this.ticket.landed,
isDeleted: this.ticket.isDeleted,
option: parseInt(this.ticket.option)
option: parseInt(this.ticket.option),
isWithoutNegatives: this.ticket.withoutNegatives
};
this.$http.post(query, params).then(res => {
this.vnApp.showMessage(
this.$t(`The ticket has been unrouted`)
);
this.card.reload();
this.$state.go('ticket.card.summary', {id: this.$params.id});
});
this.$http.post(query, params)
.then(res => {
this.ticketToMove = res.data.id;
this.vnApp.showMessage(
this.$t(`The ticket has been unrouted`)
);
})
.finally(() => {
this.$state.go('ticket.card.summary', {id: this.ticketToMove});
});
}
}

View File

@ -64,5 +64,103 @@ describe('Ticket', () => {
expect(controller.totalPriceDifference).toEqual(0.3);
});
});
describe('ticketHaveNegatives()', () => {
it('should show if ticket have any negative, have differences, but not all sale are negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 1,
movable: 5
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(true);
});
it('should not show if ticket not have any negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 2,
movable: 1
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
it('should not show if all sale are negative', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 2,
movable: 1
}
],
haveDifferences: true
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
it('should not show if ticket not have differences', () => {
controller.ticket = {
sale: {
items: [
{
item: 1,
quantity: 2,
movable: 1
},
{
item: 2,
quantity: 1,
movable: 2
}
],
haveDifferences: false
}
};
controller.ticketHaveNegatives();
expect(controller.haveNegatives).toEqual(false);
});
});
});
});

View File

@ -6,3 +6,6 @@ The ticket has been unrouted: El ticket ha sido desenrutado
Price: Precio
New price: Nuevo precio
Price difference: Diferencia de precio
Create without negatives: Crear sin negativos
Clone this ticket with the changes and only sales availables: Clona este ticket con los cambios y solo las ventas disponibles.
Movable: Movible

View File

@ -149,7 +149,10 @@ class Controller extends Section {
return this.$http.post(`Tickets/${this.id}/setDeleted`)
.then(() => this.reload())
.then(() => {
this.$state.go('ticket.index');
const isInsideTicket = this.$state.current.name.startsWith('ticket');
if (isInsideTicket)
this.$state.go('ticket.index');
this.vnApp.showSuccess(this.$t('Ticket deleted. You can undo this action within the first hour'));
});
}

View File

@ -78,9 +78,22 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
describe('deleteTicket()', () => {
it('should make a query and call vnApp.showSuccess()', () => {
jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket();
$httpBackend.flush();
expect(controller.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't inside ticket module`, () => {
jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'ticket.card.something';
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket();

View File

@ -37,6 +37,7 @@ Observation type: Tipo de observación
Original: Original
Package size: Bultos
Package type: Tipo de porte
Packing type: Encajado
Phone: Teléfono
PPU: Ud.
Price: Precio

View File

@ -20,7 +20,7 @@
on-change="$ctrl.changeState(value)">
</vn-button-menu>
<vn-ticket-descriptor-menu
ng-if="!$ctrl.isTicketModule"
ng-if="!$ctrl.isOnTicketCard"
ticket-id="$ctrl.summary.id"
parent-reload="$ctrl.reload()"
/>

View File

@ -41,9 +41,9 @@ class Controller extends Summary {
});
}
get isTicketModule() {
const path = this.$state.getCurrentPath()[1];
return path.state.name === 'ticket';
get isOnTicketCard() {
const currentState = this.$state.current.name;
return currentState.startsWith('ticket.card');
}
get isEditable() {

View File

@ -6,22 +6,17 @@
data="$ctrl.sales"
limit="20">
</vn-crud-model>
<vn-crud-model auto-load="true"
url="tickets/{{$ctrl.$params.id}}/getVolume"
data="$ctrl.volumes">
</vn-crud-model>
<mg-ajax path="tickets/{{$ctrl.$params.id}}/getTotalVolume" options="mgEdit"></mg-ajax>
<vn-vertical>
<vn-card class="vn-pa-lg">
<vn-horizontal>
<div class="totalBox">
<vn-label-value label="Total"
value="{{::edit.model.totalVolume}}">
</vn-label-value>
<vn-label-value label="Cajas"
value="{{::edit.model.totalBoxes}}">
</vn-label-value>
</div>
<div class="totalBox" ng-repeat="packingType in $ctrl.packingTypeVolume">
<vn-label-value label="Tipo"
value="{{::packingType.description}}">
</vn-label-value>
<vn-label-value label="Volumen"
value="{{::packingType.volume}}">
</vn-label-value>
</div>
</vn-horizontal>
<vn-vertical>
<vn-table model="model">
@ -29,6 +24,7 @@
<vn-tr>
<vn-th field="itemFk" number>Item</vn-th>
<vn-th field="concept" default-order="ASC">Description</vn-th>
<vn-th field="itemPackingTypeFk" number>Packing type</vn-th>
<vn-th field="quantity" number>Quantity</vn-th>
<vn-th number>m³ per quantity</vn-th>
</vn-tr>
@ -55,6 +51,7 @@
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td number>{{::sale.item.itemPackingTypeFk}}</vn-td>
<vn-td number>{{::sale.quantity}}</vn-td>
<vn-td number>{{::sale.saleVolume.volume | number:3}}</vn-td>
</vn-tr>

View File

@ -23,24 +23,19 @@ class Controller extends Section {
if (value) this.applyVolumes();
}
get volumes() {
return this._volumes;
}
set volumes(value) {
this._volumes = value;
if (value) this.applyVolumes();
}
applyVolumes() {
if (!this.sales || !this.volumes) return;
const ticket = this.sales[0].ticketFk;
this.$http.get(`Tickets/${ticket}/getVolume`).then(res => {
const saleVolume = res.data.saleVolume;
this.sales.forEach(sale => {
this.volumes.forEach(volume => {
if (sale.id === volume.saleFk)
sale.saleVolume = volume;
});
const volumes = new Map();
for (const volume of saleVolume)
volumes.set(volume.saleFk, volume);
for (const sale of this.sales)
sale.saleVolume = volumes.get(sale.id);
this.packingTypeVolume = res.data.packingTypeVolume;
});
}
}

View File

@ -33,17 +33,20 @@ describe('ticket', () => {
});
});
describe('volumes() setter', () => {
it('should set volumes property on controller an then call applyVolumes() method', () => {
jest.spyOn(controller, 'applyVolumes');
controller.volumes = [{id: 1}];
expect(controller.applyVolumes).toHaveBeenCalledWith();
});
});
describe('applyVolumes()', () => {
const ticket = 1;
const response =
{
saleVolume: [
{saleFk: 1, volume: 0.012},
{saleFk: 2, volume: 0.015}
],
packingTypeVolume: [
{code: 'V', volume: 1},
{code: 'H', volume: 2}
]
};
it(`should not apply volumes to the sales if sales property is not defined on controller`, () => {
controller.sales = [{id: 1, name: 'Sale one'}, {id: 2, name: 'Sale two'}];
@ -58,29 +61,32 @@ describe('ticket', () => {
});
it(`should apply volumes to the sales if sales and volumes properties are defined on controller`, () => {
controller.sales = [{id: 1, name: 'Sale one'}, {id: 2, name: 'Sale two'}];
controller.volumes = [{saleFk: 1, volume: 0.012}, {saleFk: 2, volume: 0.015}];
const expectedResultOne = response.saleVolume[0].volume;
const expectedResultTwo = response.saleVolume[1].volume;
$httpBackend.expectGET(`Tickets/${ticket}/getVolume`).respond(response);
controller.sales = [
{id: 1, name: 'Sale one', ticketFk: ticket},
{id: 2, name: 'Sale two'}
];
$httpBackend.flush();
expect(controller.sales[0].saleVolume.volume).toEqual(0.012);
expect(controller.sales[1].saleVolume.volume).toEqual(0.015);
expect(controller.sales[0].saleVolume.volume).toEqual(expectedResultOne);
expect(controller.sales[1].saleVolume.volume).toEqual(expectedResultTwo);
});
it(`should apply packing volumes to the sales if sales and volumes properties are defined on controller`, () => {
const expectedResultOne = response.packingTypeVolume[0].code;
const expectedResultTwo = response.packingTypeVolume[1].code;
$httpBackend.expectGET(`Tickets/${ticket}/getVolume`).respond(response);
controller.sales = [
{id: 1, name: 'Sale one', ticketFk: ticket},
{id: 2, name: 'Sale two'}
];
$httpBackend.flush();
expect(controller.packingTypeVolume[0].code).toEqual(expectedResultOne);
expect(controller.packingTypeVolume[1].code).toEqual(expectedResultTwo);
});
});
/*
it('should join the sale volumes to its respective sale', () => {
controller.ticket = {id: 1};
let response = {volumes: [
{saleFk: 1, m3: 0.008},
{saleFk: 2, m3: 0.003}
]};
$httpBackend.expectGET(`tickets/1/getVolume`).respond(response);
controller.onDataChange();
$httpBackend.flush();
expect($scope.model.data[0].volume.m3).toBe(0.008);
expect($scope.model.data[1].volume.m3).toBe(0.003);
});
*/
});
});

View File

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

View File

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

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