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

This commit is contained in:
Carlos Satorres 2024-01-24 12:50:03 +00:00
commit a594fda704
62 changed files with 1404 additions and 343 deletions

View File

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2406.01] - 2024-02-08
### Added
### Changed
### Fixed
## [2404.01] - 2024-01-25
### Added

View File

@ -0,0 +1,77 @@
const {ParameterizedSQL} = require('loopback-connector');
const {buildFilter, mergeFilters} = require('vn-loopback/util/filter');
// const {models} = require('vn-loopback/server/server');
module.exports = Self => {
Self.remoteMethod('filter', {
description:
'Find all postcodes of the model matched by postcode, town, province or country.',
accessType: 'READ',
returns: {
type: ['object'],
root: true,
},
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: 'Value to filter',
http: {source: 'query'}
},
],
http: {
path: `/filter`,
verb: 'GET',
},
});
Self.filter = async(ctx, filter, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const conn = Self.dataSource.connector;
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return {or: [
{'pc.code': {like: `%${value}%`}},
{'t.name': {like: `%${value}%`}},
{'p.name': {like: `%${value}%`}},
{'c.country': {like: `%${value}%`}}
]
};
}
}) ?? {};
filter = mergeFilters(ctx.args?.filter ?? {}, {where});
const stmts = [];
let stmt;
stmt = new ParameterizedSQL(`
SELECT
pc.code,
t.name as town,
p.name as province,
c.country
FROM
postCode pc
JOIN town t on t.id = pc.townFk
JOIN province p on p.id = t.provinceFk
JOIN country c on c.id = p.countryFk
`);
stmt.merge(conn.makeSuffix(filter));
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,103 @@
const {models} = require('vn-loopback/server/server');
describe('Postcode filter()', () => {
it('should retrieve with no filter', async() => {
const tx = await models.Postcode.beginTransaction({});
const options = {transaction: tx};
try {
const ctx = {
args: {
},
};
const results = await models.Postcode.filter(ctx, options);
expect(results.length).toBeGreaterThan(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should retrieve with filter as postcode', async() => {
const tx = await models.Postcode.beginTransaction({});
const options = {transaction: tx};
try {
const ctx = {
args: {
search: 46,
},
};
const results = await models.Postcode.filter(ctx, options);
expect(results.length).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should retrieve with filter as city', async() => {
const tx = await models.Postcode.beginTransaction({});
const options = {transaction: tx};
try {
const ctx = {
args: {
search: 'Alz',
},
};
const results = await models.Postcode.filter(ctx, options);
expect(results.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should retrieve with filter as province', async() => {
const tx = await models.Postcode.beginTransaction({});
const options = {transaction: tx};
try {
const ctx = {
args: {
search: 'one',
},
};
const results = await models.Postcode.filter(ctx, options);
expect(results.length).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should retrieve with filter as country', async() => {
const tx = await models.Postcode.beginTransaction({});
const options = {transaction: tx};
try {
const ctx = {
args: {
search: 'Ec',
},
};
const results = await models.Postcode.filter(ctx, options);
expect(results.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -20,7 +20,7 @@ module.exports = Self => {
}
});
Self.internationalExpedition = async expeditionFk => {
Self.internationalExpedition = async (expeditionFk) => {
const models = Self.app.models;
const viaexpressConfig = await models.ViaexpressConfig.findOne({

View File

@ -20,11 +20,11 @@ module.exports = Self => {
}
});
Self.renderer = async expeditionFk => {
Self.renderer = async (expeditionFk) => {
const models = Self.app.models;
const viaexpressConfig = await models.ViaexpressConfig.findOne({
fields: ['client', 'user', 'password', 'defaultWeight', 'deliveryType']
fields: ['client', 'user', 'password', 'defaultWeight', 'deliveryType', 'agencyModeFk']
});
const expedition = await models.Expedition.findOne({
@ -34,7 +34,7 @@ module.exports = Self => {
{
relation: 'ticket',
scope: {
fields: ['shipped', 'addressFk', 'clientFk', 'companyFk'],
fields: ['shipped', 'addressFk', 'clientFk', 'companyFk', 'agencyModeFk'],
include: [
{
relation: 'client',
@ -102,7 +102,6 @@ module.exports = Self => {
}
]
}
}
]
});
@ -110,13 +109,15 @@ module.exports = Self => {
const ticket = expedition.ticket();
const sender = ticket.company().client();
const shipped = ticket.shipped.toISOString();
const isInterdia = (ticket.agencyModeFk === viaexpressConfig.agencyModeFk)
const data = {
viaexpressConfig,
sender,
senderAddress: sender.defaultAddress(),
client: ticket.client(),
address: ticket.address(),
shipped
shipped,
isInterdia
};
const template = fs.readFileSync(__dirname + '/template.ejs', 'utf-8');

View File

@ -13,7 +13,7 @@
<Asegurado>0</Asegurado>
<Imprimir>0</Imprimir>
<ConDevolucionAlbaran>0</ConDevolucionAlbaran>
<Intradia>0</Intradia>
<Intradia><%= isInterdia %></Intradia>
<Observaciones></Observaciones>
<AlbaranRemitente></AlbaranRemitente>
<Modo>0</Modo>

View File

@ -1,6 +1,7 @@
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
require('../methods/postcode/filter.js')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`This postcode already exists`);

View File

@ -29,6 +29,9 @@
},
"deliveryType": {
"type": "string"
},
"agencyModeFk": {
"type": "number"
}
}
}

View File

@ -0,0 +1,589 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`sale_calculateComponent`(vSelf INT, vOption VARCHAR(25))
proc: BEGIN
/**
* Crea tabla temporal para vn.sale_recalcComponent() para recalcular los componentes
*
* @param vSelf Id de la venta
* @param vOption indica en que componente pone el descuadre, NULL en casos habituales
*/
CREATE OR REPLACE TEMPORARY TABLE tmp.recalculateSales
SELECT s.id
FROM sale s
WHERE s.id = vSelf;
CALL sale_recalcComponent(vOption);
DROP TEMPORARY TABLE tmp.recalculateSales;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`sale_checkNoComponents`(vCreatedFrom DATETIME, vCreatedTo DATETIME)
BEGIN
/**
* Comprueba que las ventas creadas entre un rango de fechas tienen componentes
*
* @param vCreatedFrom inicio del rango
* @param vCreatedTo fin del rango
*/
DECLARE v_done BOOL DEFAULT FALSE;
DECLARE vSaleFk INTEGER;
DECLARE vTicketFk INTEGER;
DECLARE vConcept VARCHAR(50);
DECLARE vCur CURSOR FOR
SELECT s.id
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
JOIN item i ON i.id = s.itemFk
JOIN itemType tp ON tp.id = i.typeFk
JOIN itemCategory ic ON ic.id = tp.categoryFk
LEFT JOIN tmp.coste c ON c.id = s.id
WHERE s.created >= vCreatedFrom AND s.created <= vCreatedTo
AND c.id IS NULL
AND t.agencyModeFk IS NOT NULL
AND t.isDeleted IS FALSE
AND t.warehouseFk = 60
AND ic.merchandise != FALSE
GROUP BY s.id;
DECLARE CONTINUE HANDLER FOR NOT FOUND
SET v_done = TRUE;
DROP TEMPORARY TABLE IF EXISTS tmp.coste;
DROP TEMPORARY TABLE IF EXISTS tmp.coste;
CREATE TEMPORARY TABLE tmp.coste
(PRIMARY KEY (id)) ENGINE = MEMORY
SELECT s.id
FROM sale s
JOIN item i ON i.id = s.itemFk
JOIN itemType tp ON tp.id = i.typeFk
JOIN itemCategory ic ON ic.id = tp.categoryFk
JOIN saleComponent sc ON sc.saleFk = s.id
JOIN component c ON c.id = sc.componentFk
JOIN componentType ct ON ct.id = c.typeFk AND ct.id = 6
WHERE s.created >= vCreatedFrom
AND ic.merchandise != FALSE;
OPEN vCur;
l: LOOP
SET v_done = FALSE;
FETCH vCur INTO vSaleFk;
IF v_done THEN
LEAVE l;
END IF;
SELECT ticketFk, concept
INTO vTicketFk, vConcept
FROM sale
WHERE id = vSaleFk;
CALL sale_calculateComponent(vSaleFk, 'renewPrices');
END LOOP;
CLOSE vCur;
DROP TEMPORARY TABLE tmp.coste;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`sale_recalcComponent`(vOption VARCHAR(25))
proc: BEGIN
/**
* Este procedimiento recalcula los componentes de un conjunto de sales,
* eliminando los componentes existentes e insertandolos de nuevo
*
* @param vOption si no se quiere forzar llamar con NULL
* @table tmp.recalculateSales (id)
*/
DECLARE vShipped DATE;
DECLARE vWarehouseFk SMALLINT;
DECLARE vAgencyModeFk INT;
DECLARE vAddressFk INT;
DECLARE vTicketFk INT;
DECLARE vLanded DATE;
DECLARE vIsEditable BOOLEAN;
DECLARE vZoneFk INTEGER;
DECLARE vDone BOOL DEFAULT FALSE;
DECLARE vCur CURSOR FOR
SELECT DISTINCT s.ticketFk
FROM tmp.recalculateSales rs
JOIN vn.sale s ON s.id = rs.id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
OPEN vCur;
l: LOOP
SET vDone = FALSE;
FETCH vCur INTO vTicketFk;
IF vDone THEN
LEAVE l;
END IF;
SELECT (hasToRecalcPrice OR ts.alertLevel IS NULL) AND t.refFk IS NULL,
t.zoneFk,
t.warehouseFk,
t.shipped,
t.addressFk,
t.agencyModeFk,
t.landed
INTO vIsEditable,
vZoneFk,
vWarehouseFk,
vShipped,
vAddressFk,
vAgencyModeFk,
vLanded
FROM ticket t
LEFT JOIN ticketState ts ON t.id = ts.ticketFk
LEFT JOIN alertLevel al ON al.id = ts.alertLevel
WHERE t.id = vTicketFk;
CALL zone_getLanded(vShipped, vAddressFk, vAgencyModeFk, vWarehouseFk, TRUE);
IF NOT EXISTS (SELECT TRUE FROM tmp.zoneGetLanded LIMIT 1) THEN
CALL util.throw(CONCAT('There is no zone for these parameters ', vTicketFk));
END IF;
IF vLanded IS NULL OR vZoneFk IS NULL THEN
UPDATE ticket t
SET t.landed = (SELECT landed FROM tmp.zoneGetLanded LIMIT 1)
WHERE t.id = vTicketFk AND t.landed IS NULL;
IF vZoneFk IS NULL THEN
SELECT zoneFk INTO vZoneFk FROM tmp.zoneGetLanded LIMIT 1;
UPDATE ticket t
SET t.zoneFk = vZoneFk
WHERE t.id = vTicketFk AND t.zoneFk IS NULL;
END IF;
END IF;
DROP TEMPORARY TABLE tmp.zoneGetLanded;
-- rellena la tabla buyUltimate con la ultima compra
CALL buyUltimate (vWarehouseFk, vShipped);
CREATE OR REPLACE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (saleFk)) ENGINE = MEMORY
SELECT s.id saleFk, vWarehouseFk warehouseFk
FROM sale s
JOIN tmp.recalculateSales rs ON s.id = rs.id
WHERE s.ticketFk = vTicketFk;
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketLot
SELECT vWarehouseFk warehouseFk, NULL available, s.itemFk, bu.buyFk, vZoneFk zoneFk
FROM sale s
JOIN tmp.recalculateSales rs ON s.id = rs.id
LEFT JOIN tmp.buyUltimate bu ON bu.itemFk = s.itemFk
WHERE s.ticketFk = vTicketFk
GROUP BY s.itemFk;
CALL catalog_componentPrepare();
CALL catalog_componentCalculate(vZoneFk, vAddressFk, vShipped, vWarehouseFk);
IF vOption IS NULL THEN
SET vOption = IF(vIsEditable, 'renewPrices', 'imbalance');
END IF;
CALL ticketComponentUpdateSale(vOption);
CALL catalog_componentPurge();
DROP TEMPORARY TABLE tmp.buyUltimate;
DROP TEMPORARY TABLE tmp.sale;
END LOOP;
CLOSE vCur;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketCalculateClon`(IN vTicketNew INT, vTicketOld INT)
BEGIN
/*
* Recalcula los componentes un ticket clonado,
* las lineas a precio cero fuerza para que tengan precio, el resto lo respeta
* @param vTicketNew nuevo ticket clonado
* @param vTicketOld icket original, a partir del qual se clonara el nuevo
*/
REPLACE INTO orderTicket(orderFk,ticketFk)
SELECT orderFk, vTicketNew
FROM orderTicket
WHERE ticketFk = vTicketOld;
-- Bionizamos lineas con Preu = 0
CREATE OR REPLACE TEMPORARY TABLE tmp.recalculateSales
(PRIMARY KEY (id)) ENGINE = MEMORY
SELECT id
FROM sale
WHERE ticketFk = vTicketNew AND price = 0;
CALL sale_recalcComponent('renewPrices');
-- Bionizamos lineas con Preu > 0
CREATE OR REPLACE TEMPORARY TABLE tmp.recalculateSales
(PRIMARY KEY (id)) ENGINE = MEMORY
SELECT id
FROM sale
WHERE ticketFk = vTicketNew AND price > 0;
CALL sale_recalcComponent('imbalance');
DROP TEMPORARY TABLE IF EXISTS tmp.recalculateSales;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketComponentUpdate`(
vTicketFk INT,
vClientFk INT,
vAgencyModeFk INT,
vAddressFk INT,
vWarehouseFk TINYINT,
vCompanyFk SMALLINT,
vShipped DATETIME,
vLanded DATE,
vIsDeleted BOOLEAN,
vHasToBeUnrouted BOOLEAN,
vOption VARCHAR(25))
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
IF (SELECT addressFk FROM ticket WHERE id = vTicketFk) <> vAddressFk THEN
UPDATE ticket t
JOIN address a ON a.id = vAddressFk
SET t.nickname = a.nickname
WHERE t.id = vTicketFk;
END IF;
UPDATE ticket t
SET
t.clientFk = vClientFk,
t.agencyModeFk = vAgencyModeFk,
t.addressFk = vAddressFk,
t.warehouseFk = vWarehouseFk,
t.companyFk = vCompanyFk,
t.landed = vLanded,
t.shipped = vShipped,
t.isDeleted = vIsDeleted
WHERE
t.id = vTicketFk;
IF vHasToBeUnrouted THEN
UPDATE ticket t SET t.routeFk = NULL
WHERE t.id = vTicketFk;
END IF;
DROP TEMPORARY TABLE IF EXISTS tmp.sale;
CREATE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (saleFk))
ENGINE = MEMORY
SELECT id AS saleFk, vWarehouseFk warehouseFk
FROM sale s WHERE s.ticketFk = vTicketFk;
CALL ticketComponentUpdateSale (vOption);
DROP TEMPORARY TABLE tmp.sale;
COMMIT;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketComponentUpdateSale`(vCode VARCHAR(25))
BEGIN
/**
* A partir de la tabla tmp.sale, crea los Movimientos_componentes
* y modifica el campo Preu de la tabla Movimientos
*
* @param i_option integer tipo de actualizacion
* @param table tmp.sale tabla memory con el campo saleFk, warehouseFk
**/
DECLARE vComponentFk INT;
IF vCode <> 'renewPrices' THEN
SELECT id INTO vComponentFk FROM component WHERE `code` = vCode;
END IF;
DELETE sc.*
FROM tmp.sale tmps
JOIN saleComponent sc ON sc.saleFk = tmps.saleFk
JOIN `component` c ON c.id = sc.componentFk
WHERE c.isRenewable;
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, tc.componentFk, tc.cost
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN tmp.ticketComponent tc ON tc.itemFk = s.itemFk AND tc.warehouseFk = tmps.warehouseFk
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
AND sc.componentFk = tc.componentFk
LEFT JOIN `component` c ON c.id = tc.componentFk
WHERE IF(sc.componentFk IS NULL AND NOT c.isRenewable, FALSE, TRUE);
-- Añadir componente venta por paquete
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT t.id, t.componentFk, t.cost
FROM (
SELECT s.id, tc.componentFk, tc.cost, MOD(s.quantity, b.packing) as resto
FROM vn.sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN cache.last_buy lb ON lb.item_id = s.itemFk AND tmps.warehouseFk = lb.warehouse_id
JOIN vn.buy b ON b.id = buy_id
JOIN tmp.ticketComponent tc ON tc.itemFk = s.itemFk AND tc.warehouseFk = tmps.warehouseFk
JOIN `component` c ON c.id = tc.componentFk AND c.code = 'salePerPackage'
LEFT JOIN (
SELECT s.id
FROM vn.sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN tmp.ticketComponent tc ON tc.itemFk = s.itemFk AND tc.warehouseFk = tmps.warehouseFk
JOIN saleComponent sc ON sc.saleFk = s.id AND sc.componentFk = tc.componentFk
JOIN `component` c ON c.id = sc.componentFk AND c.code = 'lastUnitsDiscount'
) tp ON tp.id = s.id
WHERE tp.id IS NULL
HAVING resto <> 0) t;
IF vCode <> 'renewPrices' THEN
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, vComponentFk, ROUND((s.price * (100 - s.discount) / 100) - SUM(sc.value), 3) dif
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
WHERE sc.saleFk <> vComponentFk
GROUP BY s.id
HAVING dif <> 0;
ELSE
UPDATE sale s
JOIN item i on i.id = s.itemFk
JOIN itemType it on it.id = i.typeFk
JOIN (SELECT SUM(sc.value) sumValue, sc.saleFk
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
GROUP BY sc.saleFk) sc ON sc.saleFk = s.id
SET s.price = sumValue / ((100 - s.discount) / 100)
WHERE it.code != 'PRT' ;
REPLACE INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 21, ROUND((s.price * (100 - s.discount) / 100) - SUM(value), 3) saleValue
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
LEFT JOIN saleComponent sc ON sc.saleFk = s.id
WHERE sc.componentFk != 21
GROUP BY s.id
HAVING ROUND(saleValue, 4) <> 0;
END IF;
UPDATE sale s
JOIN (
SELECT SUM(sc.value) sumValue, sc.saleFk
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
JOIN `component` c ON c.id = sc.componentFk
JOIN componentType ct on ct.id = c.typeFk AND ct.isBase
GROUP BY sc.saleFk) sc ON sc.saleFk = s.id
SET s.priceFixed = sumValue, s.isPriceFixed = 1;
DELETE sc.*
FROM saleComponent sc
JOIN tmp.sale tmps ON tmps.saleFk = sc.saleFk
JOIN sale s on s.id = sc.saleFk
JOIN item i ON i.id = s.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE it.code = 'PRT';
INSERT INTO saleComponent(saleFk, componentFk, value)
SELECT s.id, 15, s.price
FROM sale s
JOIN tmp.sale tmps ON tmps.saleFk = s.id
JOIN item i ON i.id = s.itemFK
JOIN itemType it ON it.id = i.typeFk
WHERE it.code = 'PRT' AND s.price > 0;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_checkNoComponents`(vShippedFrom DATETIME, vShippedTo DATETIME)
BEGIN
/**
* Comprueba que los tickets entre un rango de fechas tienen componentes
* y recalcula sus componentes
*
* @param vShippedFrom rango inicial de fecha
* @param vShippedTo rango final de fecha
*/
CREATE OR REPLACE TEMPORARY TABLE tmp.coste
(primary key (id)) ENGINE = MEMORY
SELECT s.id
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN itemType tp ON tp.id = i.typeFk
JOIN itemCategory ic ON ic.id = tp.categoryFk
JOIN saleComponent sc ON sc.saleFk = s.id
JOIN component c ON c.id = sc.componentFk
JOIN componentType ct ON ct.id = c.typeFk AND ct.id = 1
WHERE t.shipped BETWEEN vShippedFrom AND vShippedTo
AND ic.merchandise;
CREATE OR REPLACE TEMPORARY TABLE tmp.recalculateSales
(primary key (id)) ENGINE = MEMORY
SELECT DISTINCT s.id
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN itemType tp ON tp.id = i.typeFk
JOIN itemCategory ic ON ic.id = tp.categoryFk
LEFT JOIN tmp.coste c ON c.id = s.id
WHERE t.shipped >= vShippedFrom AND t.shipped <= vShippedTo
AND c.id IS NULL
AND ic.merchandise;
CALL sale_recalcComponent('renewPrices');
DROP TEMPORARY TABLE tmp.recalculateSales;
DROP TEMPORARY TABLE tmp.coste;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_componentMakeUpdate`(
vTicketFk INT,
vClientFk INT,
vNickname VARCHAR(50),
vAgencyModeFk INT,
vAddressFk INT,
vZoneFk INT,
vWarehouseFk INT,
vCompanyFk INT,
vShipped DATETIME,
vLanded DATE,
vIsDeleted BOOLEAN,
vHasToBeUnrouted BOOLEAN,
vOption VARCHAR(25))
BEGIN
/**
* Modifica en el ticket los campos que se le pasan por parámetro
* y cambia sus componentes
*
* @param vTicketFk Id del ticket a modificar
* @param vClientFk nuevo cliente
* @param vNickname nuevo alias
* @param vAgencyModeFk nueva agencia
* @param vAddressFk nuevo consignatario
* @param vZoneFk nueva zona
* @param vWarehouseFk nuevo almacen
* @param vCompanyFk nueva empresa
* @param vShipped nueva fecha del envio de mercancia
* @param vLanded nueva fecha de recepcion de mercancia
* @param vIsDeleted si se borra el ticket
* @param vHasToBeUnrouted si se le elimina la ruta al ticket
* @param vOption opcion para el case del proc ticketComponentUpdateSale
*/
DECLARE vPrice DECIMAL(10,2);
DECLARE vBonus DECIMAL(10,2);
CALL ticket_componentPreview (vTicketFk, vLanded, vAddressFk, vZoneFk, vWarehouseFk);
IF (SELECT addressFk FROM ticket WHERE id = vTicketFk) <> vAddressFk THEN
UPDATE ticket t
JOIN address a ON a.id = vAddressFk
SET t.nickname = a.nickname
WHERE t.id = vTicketFk;
END IF;
CALL zone_getShipped(vLanded, vAddressFk, vAgencyModeFk, TRUE);
SELECT zoneFk, price, bonus INTO vZoneFk, vPrice, vBonus
FROM tmp.zoneGetShipped
WHERE shipped BETWEEN DATE(vShipped) AND util.dayEnd(vShipped) AND warehouseFk = vWarehouseFk LIMIT 1;
UPDATE ticket t
SET
t.clientFk = vClientFk,
t.nickname = vNickname,
t.agencyModeFk = vAgencyModeFk,
t.addressFk = vAddressFk,
t.zoneFk = vZoneFk,
t.zonePrice = vPrice,
t.zoneBonus = vBonus,
t.warehouseFk = vWarehouseFk,
t.companyFk = vCompanyFk,
t.landed = vLanded,
t.shipped = vShipped,
t.isDeleted = vIsDeleted
WHERE
t.id = vTicketFk;
IF vHasToBeUnrouted THEN
UPDATE ticket t SET t.routeFk = NULL
WHERE t.id = vTicketFk;
END IF;
DROP TEMPORARY TABLE IF EXISTS tmp.sale;
CREATE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (saleFk))
ENGINE = MEMORY
SELECT id AS saleFk, vWarehouseFk warehouseFk
FROM sale s WHERE s.ticketFk = vTicketFk;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponent;
CREATE TEMPORARY TABLE tmp.ticketComponent
SELECT * FROM tmp.ticketComponentPreview;
CALL ticketComponentUpdateSale (vOption);
DROP TEMPORARY TABLE tmp.sale;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponent;
DROP TEMPORARY TABLE tmp.zoneGetShipped, tmp.ticketComponentPreview;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_recalcComponents`(vSelf INT, vOption VARCHAR(25))
proc: BEGIN
/**
* Crea tabla temporal para sale_recalcComponent() para recalcular los componentes
*
* @param vSelf Id del ticket
* @param vOption si no se quiere forzar llamar con NULL
*/
CREATE OR REPLACE TEMPORARY TABLE tmp.recalculateSales
SELECT s.id
FROM sale s
WHERE s.ticketFk = vSelf;
CALL sale_recalcComponent(vOption);
DROP TEMPORARY TABLE tmp.recalculateSales;
END$$
DELIMITER ;
TRUNCATE TABLE `vn`.`ticketUpdateAction`;
INSERT INTO `vn`.`ticketUpdateAction` (id, description, code) VALUES(1, 'Cambiar los precios en el ticket', 'renewPrice');
INSERT INTO `vn`.`ticketUpdateAction` (id, description, code) VALUES(2, 'Convertir en maná', 'mana');

View File

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`viaexpressConfig` ADD agencyModeFk int DEFAULT NULL NULL COMMENT 'Indica el agencyMode que es interdia';
ALTER TABLE `vn`.`viaexpressConfig` ADD CONSTRAINT viaexpressConfig_agencyMode_Fk FOREIGN KEY (agencyModeFK) REFERENCES vn.agencyMode(id) ON DELETE RESTRICT ON UPDATE RESTRICT;

View File

@ -0,0 +1,4 @@
REVOKE UPDATE ON TABLE `vn`.`item` FROM `employee`;
GRANT UPDATE(id, equivalent, stems, minPrice, isToPrint, family, box, category, doPhoto, image, inkFk, intrastatFk, hasMinPrice, created, comment, typeFk, generic, producerFk, description, density, relevancy, expenseFk, isActive, subName, tag5, value5, tag6, value6, tag7, value7, tag8, value8, tag9, value9, tag10, value10, minimum, upToDown, supplyResponseFk, hasKgPrice, isFloramondo, isFragile, numberOfItemsPerCask, embalageCode, quality, stemMultiplier, itemPackingTypeFk, packingOut, genericFk, packingShelve, isLaid, lastUsed, weightByPiece, weightByPiece, editorFk, recycledPlastic, nonRecycledPlastic, minQuantity) ON TABLE `vn`.`item` TO `employee`;

View File

@ -0,0 +1,12 @@
ALTER TABLE `vn`.`company` MODIFY COLUMN `supplierAccountFk` mediumint(8) unsigned DEFAULT NULL NULL COMMENT 'Cuenta por defecto para ingresos desde este pais';
ALTER TABLE `vn`.`supplierAccount`
ADD COLUMN `countryFk` mediumint(8) unsigned DEFAULT NULL,
ADD CONSTRAINT `fk_supplierAccount_country`
FOREIGN KEY (`countryFk`) REFERENCES `country` (`id`) ON UPDATE CASCADE;
ALTER TABLE `vn`.`supplierAccount`
ADD UNIQUE KEY `uk_supplier_country` (`supplierFk`, `countryFk`);

View File

@ -1,4 +1,4 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId) VALUES
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId) VALUES
('VnRole','*','READ','ALLOW','ROLE','employee'),
('VnRole','*','WRITE','ALLOW','ROLE','it');

View File

@ -1,2 +1,2 @@
ALTER TABLE vn.productionConfig ADD itemPreviousDefaultSize int NULL COMMENT 'Altura por defecto para los artículos de previa';
UPDATE IGNORE vn.productionConfig SET itemPreviousDefaultSize = 40 WHERE id = 1;
ALTER TABLE `vn`.`productionConfig` ADD itemPreviousDefaultSize int NULL COMMENT 'Altura por defecto para los artículos de previa';
UPDATE IGNORE `vn`.`productionConfig` SET itemPreviousDefaultSize = 40 WHERE id = 1;

View File

@ -0,0 +1,9 @@
UPDATE `vn`.`supplierAccount` sa
JOIN `vn`.`country` c ON sa.countryFk = c.id AND c.code = 'FR'
SET countryFk = c.id
WHERE iban = 'FR7630003012690002801121597';
UPDATE `vn`.`supplierAccount` sa
JOIN `vn`.`country` c ON sa.countryFk = c.id AND c.code = 'PT'
SET countryFk = c.id
WHERE iban = 'PT50001000005813059150168';

View File

@ -1,2 +1,2 @@
ALTER TABLE vn.invoiceOutConfig
ALTER TABLE `vn`.`invoiceOutConfig`
ADD IF NOT EXISTS refLen TINYINT UNSIGNED DEFAULT 5 NOT NULL COMMENT 'Invoice reference identifier length';

View File

View File

@ -0,0 +1,3 @@
GRANT EXECUTE ON PROCEDURE util.tx_commit TO guest;
GRANT EXECUTE ON PROCEDURE util.tx_rollback TO guest;
GRANT EXECUTE ON PROCEDURE util.tx_start TO guest;

View File

@ -0,0 +1,85 @@
DROP PROCEDURE IF EXISTS vn.travel_cloneWithEntries;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`travel_cloneWithEntries`(
IN vTravelFk INT,
IN vDateStart DATE,
IN vDateEnd DATE,
IN vWarehouseOutFk INT,
IN vWarehouseInFk INT,
IN vRef VARCHAR(255),
IN vAgencyModeFk INT,
OUT vNewTravelFk INT)
BEGIN
/**
* Clona un travel junto con sus entradas y compras
* @param vTravelFk travel plantilla a clonar
* @param vDateStart fecha del shipment del nuevo travel
* @param vDateEnd fecha del landing del nuevo travel
* @param vWarehouseOutFk warehouse del salida del nuevo travel
* @param vWarehouseInFk warehouse de landing del nuevo travel
* @param vRef referencia del nuevo travel
* @param vAgencyModeFk del nuevo travel
* @param vNewTravelFk id del nuevo travel
*/
DECLARE vNewEntryFk INT;
DECLARE vEvaNotes VARCHAR(255);
DECLARE vDone BOOL;
DECLARE vAuxEntryFk INT;
DECLARE vTx BOOLEAN DEFAULT !@@in_transaction;
DECLARE vRsEntry CURSOR FOR
SELECT e.id
FROM entry e
JOIN travel t ON t.id = e.travelFk
WHERE e.travelFk = vTravelFk;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
CALL util.tx_rollback(vTx);
RESIGNAL;
END;
CALL util.tx_start(vTx);
INSERT INTO travel (shipped, landed, warehouseInFk, warehouseOutFk, agencyModeFk, `ref`, isDelivered, isReceived, m3, cargoSupplierFk, kg,clonedFrom)
SELECT vDateStart, vDateEnd, vWarehouseInFk, vWarehouseOutFk, vAgencyModeFk, vRef, isDelivered, isReceived, m3,cargoSupplierFk, kg,vTravelFk
FROM travel
WHERE id = vTravelFk;
SET vNewTravelFk = LAST_INSERT_ID();
SET vDone = FALSE;
SET @isModeInventory = TRUE;
OPEN vRsEntry;
l: LOOP
SET vDone = FALSE;
FETCH vRsEntry INTO vAuxEntryFk;
IF vDone THEN
LEAVE l;
END IF;
CALL entry_cloneHeader(vAuxEntryFk, vNewEntryFk, vNewTravelFk);
CALL entry_copyBuys(vAuxEntryFk, vNewEntryFk);
SELECT evaNotes INTO vEvaNotes
FROM entry
WHERE id = vAuxEntryFk;
UPDATE entry
SET evaNotes = vEvaNotes
WHERE id = vNewEntryFk;
END LOOP;
SET @isModeInventory = FALSE;
CLOSE vRsEntry;
CALL util.tx_commit(vTx);
END$$
DELIMITER ;

View File

@ -0,0 +1,15 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `util`.`tx_commit`(IN tx BOOL)
BEGIN
/**
* Procedimiento para confirmar los cambios asociados a una transacción
*
* @param tx BOOL es true si existe transacción asociada
*/
IF tx THEN
COMMIT;
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,15 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `util`.`tx_rollback`(tx BOOL)
BEGIN
/**
* Procedimiento para deshacer los cambios asociados a una transacción
*
* @param tx BOOL es true si existe transacción asociada
*/
IF tx THEN
ROLLBACK;
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,17 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `util`.`tx_start`(tx BOOL)
BEGIN
/**
* Procedimiento para iniciar una transacción
*
* @param tx BOOL es true si existe transacción asociada
*/
IF tx THEN
START TRANSACTION;
END IF;
END$$
DELIMITER ;

View File

@ -27,11 +27,8 @@ describe('Item Edit basic data path', () => {
it(`should edit the item basic data and confirm the item data was edited`, async() => {
const values = {
name: 'Rose of Purity',
longName: 'RS Rose of Purity',
type: 'Anthurium',
intrastat: 'Coral y materiales similares',
origin: 'Spain',
relevancy: 1,
generic: 'Pallet',
isActive: false,

View File

@ -270,8 +270,8 @@ class VnMySQL extends MySQL {
isLoggable(model) {
const Model = this.getModelDefinition(model).model;
const settings = Model.definition.settings;
return settings.base && settings.base === 'Loggable';
const {settings} = Model.definition;
return settings?.mixins?.Loggable;
}
invokeMethod(method, args, model, ctx, opts, cb) {
@ -291,7 +291,7 @@ class VnMySQL extends MySQL {
}
try {
const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
const userId = opts.httpCtx && opts.httpCtx.active?.accessToken?.userId;
if (userId) {
const user = await Model.app.models.VnUser.findById(userId, {fields: ['name']}, opts);
await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts);

View File

@ -1,10 +1,11 @@
const SalixError = require('../../util/salixError');
const UserError = require('../../util/user-error');
const logToConsole = require('strong-error-handler/lib/logger');
module.exports = function() {
return function(err, req, res, next) {
// Thrown user errors
if (err instanceof UserError) {
if (err instanceof SalixError) {
err.message = req.__(err.message, ...err.translateArgs);
return next(err);
}
@ -13,7 +14,7 @@ module.exports = function() {
if (err.statusCode == 422) {
try {
let code;
let messages = err.details.messages;
let {messages} = err.details;
for (code in messages) break;
err.message = req.__(messages[code][0]);
return next(err);

View File

@ -1,7 +1,8 @@
module.exports = class ForbiddenError extends Error {
const SalixError = require('./salixError');
module.exports = class ForbiddenError extends SalixError {
constructor(message, code, ...translateArgs) {
super(message);
this.name = 'ForbiddenError';
this.name = ForbiddenError.name;
this.statusCode = 403;
this.code = code;
this.translateArgs = translateArgs;

View File

@ -0,0 +1,5 @@
module.exports = class SalixError extends Error {
constructor(message) {
super(message);
}
};

View File

@ -4,10 +4,11 @@
* the final user, so they cannot contain sensitive data and must
* be understandable by people who do not have a technical profile.
*/
module.exports = class UserError extends Error {
const SalixError = require('./salixError');
module.exports = class UserError extends SalixError {
constructor(message, code, ...translateArgs) {
super(message);
this.name = 'UserError';
this.name = UserError.name;
this.statusCode = 400;
this.code = code;
this.translateArgs = translateArgs;

View File

@ -49,7 +49,8 @@
<div class="ellipsize">
<span ng-show="::address.postalCode">{{::address.postalCode}} -</span>
<span ng-show="::address.city">{{::address.city}},</span>
{{::address.province.name}}
<span ng-show="::address.province.name">{{::address.province.name}},</span>
{{::address.province.country.country}}
</div>
<div class="ellipsize">
{{::address.phone}}<span ng-if="::address.mobile">, </span>

View File

@ -33,7 +33,13 @@ class Controller extends Section {
}, {
relation: 'province',
scope: {
fields: ['id', 'name']
fields: ['id', 'name', 'countryFk'],
include: {
relation: 'country',
scope: {
fields: ['id', 'country']
}
}
}
}
]

View File

@ -18,22 +18,6 @@
</vn-crud-model>
<form name="form" ng-submit="watcher.submit()" ng-cloak class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Name"
ng-model="$ctrl.item.name"
vn-name="name"
rule
vn-focus>
</vn-textfield>
<vn-textfield
label="Full name"
ng-model="$ctrl.item.longName"
vn-name="longName"
rule
info="Full name calculates based on tags 1-3. Is not recommended to change it manually">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
url="ItemTypes"
@ -52,6 +36,34 @@
</div>
</tpl-item>
</vn-autocomplete>
<vn-textfield
label="Reference"
ng-model="$ctrl.item.comment"
vn-name="comment"
rule>
</vn-textfield>
<vn-input-number
min="0"
label="Relevancy"
ng-model="$ctrl.item.relevancy"
vn-name="relevancy"
rule>
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
min="0"
label="stems"
ng-model="$ctrl.item.stems"
vn-name="stems"
rule>
</vn-input-number>
<vn-input-number
min="0"
label="Multiplier"
ng-model="$ctrl.item.stemMultiplier"
vn-name="stemMultiplier">
</vn-input-number>
<vn-autocomplete
label="Generic"
url="Items/withName"
@ -105,63 +117,10 @@
url="Expenses"
label="Expense"
ng-model="$ctrl.item.expenseFk"
vn-name="expense"
vn-name="expence"
initial-data="$ctrl.item.expense">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
data="originsData"
label="Origin"
show-field="name"
value-field="id"
ng-model="$ctrl.item.originFk"
vn-name="origin"
initial-data="$ctrl.item.origin">
</vn-autocomplete>
<vn-input-number
min="0"
label="Size"
ng-model="$ctrl.item.size"
vn-name="size"
rule>
</vn-input-number>
<vn-textfield
label="Reference"
ng-model="$ctrl.item.comment"
vn-name="comment"
rule>
</vn-textfield>
<vn-input-number
min="0"
label="Relevancy"
ng-model="$ctrl.item.relevancy"
vn-name="relevancy"
rule>
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
min="0"
label="stems"
ng-model="$ctrl.item.stems"
vn-name="stems"
rule>
</vn-input-number>
<vn-input-number
min="0"
label="Multiplier"
ng-model="$ctrl.item.stemMultiplier"
vn-name="stemMultiplier">
</vn-input-number>
<vn-input-number
min="1"
label="Minimum sales quantity"
ng-model="$ctrl.item.minQuantity"
vn-name="minQuantity"
rule>
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
min="0"
@ -192,14 +151,6 @@
rule>
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
label="Description"
ng-model="$ctrl.item.description"
vn-name="description"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-check
label="Active"
@ -224,6 +175,14 @@
info="This item does need a photo">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
label="Description"
ng-model="$ctrl.item.description"
vn-name="description"
rule>
</vn-textarea>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit

View File

@ -20,6 +20,9 @@
},
"beneficiary": {
"type": "string"
},
"supplierFk": {
"type": "string"
}
},
"relations": {

View File

@ -14,17 +14,30 @@ module.exports = Self => {
}
try {
const salesFilter = {
where: {id: {inq: salesIds}},
include: {
relation: 'components',
scope: {
fields: ['saleFk', 'componentFk', 'value']
let sales;
let services;
if (salesIds && salesIds.length) {
sales = await models.Sale.find({
where: {id: {inq: salesIds}},
include: {
relation: 'components',
scope: {
fields: ['saleFk', 'componentFk', 'value']
}
}
}
};
const sales = await models.Sale.find(salesFilter, myOptions);
let ticketsIds = [...new Set(sales.map(sale => sale.ticketFk))];
}, myOptions);
}
if (servicesIds && servicesIds.length) {
services = await models.TicketService.find({
where: {id: {inq: servicesIds}}
}, myOptions);
}
let ticketsIds = sales ?
[...new Set(sales.map(sale => sale.ticketFk))] :
[...new Set(services.map(service => service.ticketFk))];
const mappedTickets = new Map();
@ -39,32 +52,28 @@ module.exports = Self => {
newTickets.push(newTicket);
mappedTickets.set(ticketId, newTicket.id);
}
if (sales) {
for (const sale of sales) {
const newTicketId = mappedTickets.get(sale.ticketFk);
for (const sale of sales) {
const newTicketId = mappedTickets.get(sale.ticketFk);
const createdSale = await models.Sale.create({
ticketFk: newTicketId,
itemFk: sale.itemFk,
quantity: negative ? - sale.quantity : sale.quantity,
concept: sale.concept,
price: sale.price,
discount: sale.discount,
}, myOptions);
const createdSale = await models.Sale.create({
ticketFk: newTicketId,
itemFk: sale.itemFk,
quantity: negative ? - sale.quantity : sale.quantity,
concept: sale.concept,
price: sale.price,
discount: sale.discount,
}, myOptions);
const components = sale.components();
for (const component of components)
component.saleFk = createdSale.id;
const components = sale.components();
for (const component of components)
component.saleFk = createdSale.id;
await models.SaleComponent.create(components, myOptions);
await models.SaleComponent.create(components, myOptions);
}
}
if (servicesIds && servicesIds.length) {
const servicesFilter = {
where: {id: {inq: servicesIds}}
};
const services = await models.TicketService.find(servicesFilter, myOptions);
if (services) {
for (const service of services) {
const newTicketId = mappedTickets.get(service.ticketFk);

View File

@ -6,7 +6,6 @@ module.exports = Self => {
{
arg: 'salesIds',
type: ['number'],
required: true
},
{
arg: 'servicesIds',

View File

@ -44,24 +44,7 @@ describe('Sale refund()', () => {
const tickets = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({
where: {
id: tickets[0].id
},
include: [
{
relation: 'ticketSales',
scope: {
include: {
relation: 'components'
}
}
},
{
relation: 'ticketServices',
}
]
}, options);
const refundedTicket = await getTicketRefund(tickets[0].id, options);
const ticketsAfter = await models.Ticket.find({}, options);
const salesLength = refundedTicket.ticketSales().length;
const componentsLength = refundedTicket.ticketSales()[0].components().length;
@ -77,4 +60,42 @@ describe('Sale refund()', () => {
throw e;
}
});
it('should create a ticket without sales', async() => {
const servicesIds = [4];
const tx = await models.Sale.beginTransaction({});
const options = {transaction: tx};
try {
const tickets = await models.Sale.refund(ctx, null, servicesIds, withWarehouse, options);
const refundedTicket = await getTicketRefund(tickets[0].id, options);
expect(refundedTicket).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
async function getTicketRefund(id, options) {
return models.Ticket.findOne({
where: {
id
},
include: [
{
relation: 'ticketSales',
scope: {
include: {
relation: 'components'
}
}
},
{
relation: 'ticketServices',
}
]
}, options);
}

View File

@ -74,8 +74,8 @@ module.exports = Self => {
},
{
arg: 'option',
type: 'number',
description: 'Action id'
type: 'string',
description: 'Action code'
},
{
arg: 'isWithoutNegatives',

View File

@ -60,7 +60,7 @@ describe('ticket componentUpdate()', () => {
shipped: today,
landed: tomorrow,
isDeleted: false,
option: 1,
option: 'renewPrices',
isWithoutNegatives: false
};
@ -110,7 +110,7 @@ describe('ticket componentUpdate()', () => {
shipped: today,
landed: tomorrow,
isDeleted: false,
option: 1,
option: 'renewPrices',
isWithoutNegatives: false
};
@ -176,7 +176,7 @@ describe('ticket componentUpdate()', () => {
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
option: 'renewPrices',
isWithoutNegatives: true
};
@ -235,7 +235,7 @@ describe('ticket componentUpdate()', () => {
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
option: 'renewPrices',
isWithoutNegatives: false,
keepPrice: true
};
@ -288,7 +288,7 @@ describe('ticket componentUpdate()', () => {
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
option: 'renewPrices',
isWithoutNegatives: false,
keepPrice: false
};

View File

@ -76,7 +76,7 @@
<vn-radio
ng-model="$ctrl.ticket.option"
label="{{::action.description}}"
val={{::action.id}}>
val={{::action.code}}>
</vn-radio>
</div>
</div>

View File

@ -25,12 +25,7 @@ class Controller extends Component {
loadDefaultTicketAction() {
const isSalesAssistant = this.aclService.hasAny(['salesAssistant']);
const defaultOption = isSalesAssistant ? 'turnInMana' : 'changePrice';
const filter = {where: {code: defaultOption}};
this.$http.get(`TicketUpdateActions`, {filter}).then(response => {
return this.ticket.option = response.data[0].id;
});
this.ticket.option = isSalesAssistant ? 'mana' : 'buyerDiscount';
}
onStepChange() {
@ -112,7 +107,7 @@ class Controller extends Component {
shipped: this.ticket.shipped,
landed: this.ticket.landed,
isDeleted: this.ticket.isDeleted,
option: parseInt(this.ticket.option),
option: this.ticket.option,
isWithoutNegatives: this.ticket.withoutNegatives,
withWarningAccept: this.ticket.withWarningAccept,
keepPrice: false

View File

@ -1,6 +1,5 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const UserError = require('vn-loopback/util/user-error');
const loggable = require('vn-loopback/util/log');
module.exports = Self => {
Self.remoteMethodCtx('cloneWithEntries', {
@ -11,8 +10,9 @@ module.exports = Self => {
type: 'number',
required: true,
description: 'The original travel id',
http: {source: 'path'}
}],
http: {source: 'path'},
},
],
returns: {
type: 'object',
description: 'The new cloned travel id',
@ -24,61 +24,75 @@ module.exports = Self => {
}
});
Self.cloneWithEntries = async(ctx, id) => {
Self.cloneWithEntries = async(ctx, id, options) => {
const conn = Self.dataSource.connector;
const travel = await Self.findById(id, {
fields: [
'id',
'shipped',
'landed',
'warehouseInFk',
'warehouseOutFk',
'agencyModeFk',
'ref'
]
});
const started = Date.vnNew();
const ended = Date.vnNew();
const myOptions = {};
let tx = options?.transaction;
if (!travel)
throw new UserError('Travel not found');
try {
if (typeof options == 'object')
Object.assign(myOptions, options);
let stmts = [];
let stmt;
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
stmt = new ParameterizedSQL(
`CALL travel_cloneWithEntries(?, ?, ?, ?, ?, ?, ?, @vTravelFk)`, [
id,
started,
ended,
travel.warehouseOutFk,
travel.warehouseInFk,
travel.ref,
travel.agencyModeFk
]
);
stmts.push(stmt);
const newTravelIndex = stmts.push('SELECT @vTravelFk AS id') - 1;
const travel = await Self.findById(id, {
fields: [
'id',
'shipped',
'landed',
'warehouseInFk',
'warehouseOutFk',
'agencyModeFk',
'ref'
]
});
const started = Date.vnNew();
const ended = Date.vnNew();
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const [lastInsert] = result[newTravelIndex];
if (!travel)
throw new UserError('Travel not found');
if (!lastInsert.id)
throw new UserError('Unable to clone this travel');
let stmts = [];
let stmt;
stmt = new ParameterizedSQL(
`CALL travel_cloneWithEntries(?, ?, ?, ?, ?, ?, ?, @vTravelFk)`, [
id,
started,
ended,
travel.warehouseOutFk,
travel.warehouseInFk,
travel.ref,
travel.agencyModeFk
]
);
stmts.push(stmt);
const newTravelIndex = stmts.push('SELECT @vTravelFk AS id') - 1;
const newTravel = await Self.findById(lastInsert.id, {
fields: [
'id',
'shipped',
'landed',
'warehouseInFk',
'warehouseOutFk',
'agencyModeFk',
'ref'
]
});
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
const [lastInsert] = result[newTravelIndex];
return newTravel.id;
if (!lastInsert.id)
throw new UserError('Unable to clone this travel');
const newTravel = await Self.findById(lastInsert.id, {
fields: [
'id',
'shipped',
'landed',
'warehouseInFk',
'warehouseOutFk',
'agencyModeFk',
'ref'
]
}, myOptions);
return newTravel.id;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -5,73 +5,36 @@ describe('Travel cloneWithEntries()', () => {
const travelId = 5;
const currentUserId = 1102;
const ctx = {req: {accessToken: {userId: currentUserId}}};
let travelBefore;
let newTravelId;
// afterAll(async() => {
// try {
// const entries = await models.Entry.find({
// where: {
// travelFk: newTravelId
// }
// });
// const entriesId = entries.map(entry => entry.id);
// // Destroy all entries buys
// await models.Buy.destroyAll({
// where: {
// entryFk: {inq: entriesId}
// }
// });
// // Destroy travel entries
// await models.Entry.destroyAll({
// where: {
// travelFk: newTravelId
// }
// });
// // Destroy new travel
// await models.Travel.destroyById(newTravelId);
// // Restore original travel shipped & landed
// const travel = await models.Travel.findById(travelId);
// await travel.updateAttributes({
// shipped: travelBefore.shipped,
// landed: travelBefore.landed
// });
// } catch (error) {
// console.error(error);
// }
// });
it(`should clone the travel and the containing entries`, async() => {
pending('#2687 - Cannot make a data rollback because of the triggers');
const tx = await models.Travel.beginTransaction({
});
const warehouseThree = 3;
const agencyModeOne = 1;
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
try {
const options = {transaction: tx};
newTravelId = await models.Travel.cloneWithEntries(ctx, travelId, options);
const travelEntries = await models.Entry.find({
where: {
travelFk: newTravelId
}
}, options);
const newTravel = await models.Travel.findById(travelId);
travelBefore = await models.Travel.findById(travelId);
await travelBefore.updateAttributes({
shipped: yesterday,
landed: yesterday
});
expect(newTravelId).not.toEqual(travelId);
expect(newTravel.ref).toEqual('fifth travel');
expect(newTravel.warehouseInFk).toEqual(warehouseThree);
expect(newTravel.warehouseOutFk).toEqual(warehouseThree);
expect(newTravel.agencyModeFk).toEqual(agencyModeOne);
expect(travelEntries.length).toBeGreaterThan(0);
newTravelId = await models.Travel.cloneWithEntries(ctx, travelId);
const travelEntries = await models.Entry.find({
where: {
travelFk: newTravelId
}
});
await tx.rollback();
const travelRemoved = await models.Travel.findById(newTravelId, options);
const newTravel = await models.Travel.findById(travelId);
expect(newTravelId).not.toEqual(travelId);
expect(newTravel.ref).toEqual('fifth travel');
expect(newTravel.warehouseInFk).toEqual(warehouseThree);
expect(newTravel.warehouseOutFk).toEqual(warehouseThree);
expect(newTravel.agencyModeFk).toEqual(agencyModeOne);
expect(travelEntries.length).toBeGreaterThan(0);
expect(travelRemoved).toBeNull();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
});
});

View File

@ -3,7 +3,7 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateTimeEntry', {
description: 'Updates a time entry for a worker if the user role is above the worker',
accessType: 'READ',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',

View File

@ -41,15 +41,16 @@ module.exports = Self => {
}
const stmt = new ParameterizedSQL(`
SELECT *
SELECT *
FROM(
SELECT DISTINCT w.id, w.code, u.name, u.nickname, u.active, b.departmentFk
SELECT w.id, w.code, u.name, u.nickname, u.active, wd.departmentFk
FROM worker w
JOIN account.user u ON u.id = w.id
LEFT JOIN business b ON b.workerFk = w.id
LEFT JOIN workerDepartment wd ON wd.workerFk = w.id
) w`);
stmt.merge(conn.makeSuffix(filter));
return conn.executeStmt(stmt);
};

4
package-lock.json generated
View File

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

View File

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

View File

@ -1,14 +1,33 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
const attachment = new Component('attachment');
const db = require('../../../core/database');
module.exports = {
name: 'letter-debtor-nd',
async serverPrefetch() {
this.debtor = await this.fetchDebtor(this.id, this.companyId);
if (!this.debtor)
throw new Error('Something went wrong');
this.debtor = await db.findOne(`
SELECT sa.id,
sa.iban,
be.name bankName,
sa.countryFk,
c.countryFk
FROM supplierAccount sa
JOIN bankEntity be ON sa.bankEntityFk = be.id
LEFT JOIN company co ON co.supplierAccountFk = sa.id
JOIN client c ON c.countryFk = sa.countryFk
WHERE c.id = ?;
`, [this.id]);
if (!this.debtor) {
this.debtor = await db.findOne(`
SELECT sa.iban,
be.name bankName
FROM supplierAccount sa
JOIN bankEntity be ON sa.bankEntityFk = be.id
JOIN company co ON co.supplierAccountFk = sa.id
WHERE co.id = ?;
`, [this.companyId]);
}
},
data() {
return {

View File

@ -0,0 +1,21 @@
subject: Reminder of Outstanding Balance Notice
title: Reminder Notice
sections:
introduction:
title: Dear Customer
description: We are writing to you once again to inform you that your debt with our company remains unpaid, as you can verify in the attached statement.
terms: Since the agreed payment deadlines have significantly passed, there should be no further delay in settling the outstanding amount.
payMethod:
description: To do so, you have the following payment options
options:
- Online payment through our website.
- Deposit or transfer to the account number provided at the bottom of this letter, indicating your customer number.
legalAction:
description: If this payment reminder is not heeded, we will be compelled to initiate the necessary legal actions, which may include
options:
- Inclusion in negative credit and financial solvency records.
- Legal proceedings.
- Debt assignment to a debt collection agency.
contactPhone: For inquiries, you can reach us at <strong>96 324 21 00</strong>.
conclusion: We look forward to hearing from you. <br/> Thank you for your attention.
transferAccount: Bank Transfer Details

View File

@ -0,0 +1,26 @@
subject: Réitération de l'avis de solde débiteur
title: Avis réitéré
sections:
introduction:
title: Cher client
description: Nous vous écrivons à nouveau pour vous informer qu'il est toujours en attente
votre dette envers notre société, comme vous pouvez le voir dans le relevé ci-joint.
terms: Étant donné que les délais de paiement convenus sont largement dépassés, il n'est pas approprié
retard plus important dans le règlement du montant dû.
payMethod:
description: Pour cela, vous disposez des modes de paiement suivants
options:
- Paiement en ligne depuis notre site internet.
- Revenu ou virement sur le numéro de compte que nous détaillons en bas de ce courrier,
indiquant le numéro de client.
legalAction:
description: Si cette obligation de paiement n'est pas remplie, nous serons contraints de
d'engager les actions judiciaires qui se déroulent, parmi lesquelles
options:
- Inclusion dans les dossiers négatifs sur la solvabilité financière et le crédit.
- Réclamation judiciaire.
- Cession de créance à une société de gestion de recouvrement.
contactPhone: Pour toute demande, vous pouvez nous contacter au <strong>96
324 21 00</strong>.
conclusion: En attente de vos nouvelles. <br/> Merci pour ton attention.
transferAccount: Données pour virement bancaire

View File

@ -0,0 +1,26 @@
subject: Reiteração de aviso de saldo devedor
title: Aviso reiterado
sections:
introduction:
title: Estimado cliente
description: Estamos escrevendo para você novamente para informar que ainda está pendente
sua dívida para com nossa empresa, conforme demonstrativo anexo.
terms: Dado que os prazos de pagamento acordados são largamente excedidos, não é adequado
maior atraso na liquidação do valor devido.
payMethod:
description: Para isso você tem as seguintes formas de pagamento
options:
- Pagamento online em nosso site.
- Renda ou transferência para o número da conta que detalhamos no final desta carta,
indicando o número do cliente.
legalAction:
description: Se esta obrigação de pagamento não for cumprida, seremos obrigados a
para iniciar as ações legais que procedem, entre as quais estão
options:
- Inclusão em processos negativos de solvência financeira e de crédito.
- Reivindicação judicial.
- Cessão de dívida a uma empresa de gestão de cobranças.
contactPhone: Para consultas, você pode entrar em contato conosco em <strong>96
324 21 00</strong>.
conclusion: Aguardando suas notícias. <br/> Agradecimentos para sua atenção.
transferAccount: Dados para transferência bancária

View File

@ -1,10 +1,9 @@
SELECT
c.dueDay,
c.iban,
sa.iban,
be.name AS bankName
FROM client c
JOIN company AS cny
JOIN supplierAccount AS sa ON sa.id = cny.supplierAccountFk
JOIN bankEntity be ON be.id = sa.bankEntityFk
WHERE c.id = ? AND cny.id = ?
SELECT c.dueDay,
sa.iban,
be.name bankName
FROM client c
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
JOIN bankEntity be ON be.id = sa.bankEntityFk
JOIN company cny
WHERE c.id = ?
AND cny.id = ?

View File

@ -1,14 +1,33 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
const attachment = new Component('attachment');
const db = require('../../../core/database');
module.exports = {
name: 'letter-debtor-st',
async serverPrefetch() {
this.debtor = await this.fetchDebtor(this.id, this.companyId);
if (!this.debtor)
throw new Error('Something went wrong');
this.debtor = await db.findOne(`
SELECT sa.id,
sa.iban,
be.name bankName,
sa.countryFk,
c.countryFk
FROM supplierAccount sa
JOIN bankEntity be ON sa.bankEntityFk = be.id
LEFT JOIN company co ON co.supplierAccountFk = sa.id
JOIN client c ON c.countryFk = sa.countryFk
WHERE c.id = ?;
`, [this.id]);
if (!this.debtor) {
this.debtor = await db.findOne(`
SELECT sa.iban,
be.name bankName
FROM supplierAccount sa
JOIN bankEntity be ON sa.bankEntityFk = be.id
JOIN company co ON co.supplierAccountFk = sa.id
WHERE co.id = ?;
`, [this.companyId]);
}
},
data() {
return {

View File

@ -0,0 +1,11 @@
subject: Initial Notice for Outstanding Balance
title: Initial Notice for Outstanding Balance
sections:
introduction:
title: Dear Customer
description: Through this letter, we would like to inform you that, according to our accounting records, your account has an outstanding balance that needs to be settled.
checkExtract: We kindly request you to verify that the attached statement corresponds to the information you have. Our administration department will be happy to clarify any questions you may have and provide any documents you may request.
checkValidData: If, upon reviewing the provided information, everything appears to be accurate, we kindly ask you to proceed with rectifying your situation.
payMethod: If you prefer not to visit our offices in person, you can make the payment through a bank transfer to the account listed at the bottom of this communication, indicating your customer number. Alternatively, you can make the payment online through our website.
conclusion: We sincerely appreciate your kind cooperation.
transferAccount: Bank Transfer Details

View File

@ -1,10 +1,9 @@
SELECT
c.dueDay,
c.iban,
sa.iban,
be.name AS bankName
FROM client c
JOIN company AS cny
JOIN supplierAccount AS sa ON sa.id = cny.supplierAccountFk
JOIN bankEntity be ON be.id = sa.bankEntityFk
WHERE c.id = ? AND cny.id = ?
SELECT c.dueDay,
sa.iban,
be.name bankName
FROM client c
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
JOIN bankEntity be ON be.id = sa.bankEntityFk
JOIN company cny
WHERE c.id = ?
AND cny.id = ?

View File

@ -27,8 +27,8 @@
<tr>
<td>{{$t('supplier.identifier')}}</td>
<th>
<div>ES89000B97367486</div>
<div>B97367486-000</div>
<div>{{supplier.iban}}</div>
<div>{{supplier.nif}}</div>
</th>
</tr>
<tr>

View File

@ -1,4 +1,5 @@
const vnReport = require('../../../core/mixins/vn-report.js');
const db = require('../../../core/database');
module.exports = {
name: 'sepa-core',
@ -18,5 +19,16 @@ module.exports = {
type: Number,
required: true
}
},
methods: {
getSupplierCif() {
return db.findOne(`
SELECT sa.iban, s.nif
FROM supplierAccount sa
JOIN company co ON co.supplierAccountFk = sa.id
JOIN supplier s ON sa.supplierFk = s.id
WHERE co.id = ?`) [this.companyId];
}
}
};

View File

@ -1,17 +1,27 @@
SELECT
m.code mandateCode,
s.name,
s.street,
sc.country,
s.postCode,
s.city,
sp.name province
FROM client c
LEFT JOIN mandate m ON m.clientFk = c.id
AND m.companyFk = ? AND m.finished IS NULL
m.code mandateCode,
s.name,
s.street,
sc.country,
s.postCode,
s.city,
sp.name province,
s.nif,
sa.iban,
sa.supplierFk,
be.name bankName
FROM
client c
LEFT JOIN mandate m ON m.clientFk = c.id AND m.companyFk = ? AND m.finished IS NULL
LEFT JOIN supplier s ON s.id = m.companyFk
LEFT JOIN country sc ON sc.id = s.countryFk
LEFT JOIN province sp ON sp.id = s.provinceFk
LEFT JOIN province p ON p.id = c.provinceFk
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
ORDER BY m.created DESC LIMIT 1
LEFT JOIN supplierAccount sa ON sa.supplierFk = s.id
LEFT JOIN bankEntity be ON sa.bankEntityFk = be.id
WHERE
(m.companyFk = ? OR m.companyFk IS NULL)
AND (c.id = ? OR (c.id IS NULL AND c.countryFk = sa.countryFk))
ORDER BY
m.created DESC
LIMIT 1;