Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5739-dockerRefactor

This commit is contained in:
Alex Moreno 2024-01-12 11:01:14 +01:00
commit ba247f4767
38 changed files with 2424 additions and 92 deletions

View File

@ -17,10 +17,6 @@
"type": "string",
"required": true
},
"path": {
"type": "string",
"required": true
},
"code": {
"type": "string",
"required": true

View File

@ -258,18 +258,20 @@ module.exports = function(Self) {
class Mailer {
async send(verifyOptions, cb) {
const url = new URL(verifyOptions.verifyHref);
if (process.env.NODE_ENV) url.port = '';
try {
const url = new URL(verifyOptions.verifyHref);
if (process.env.NODE_ENV) url.port = '';
const params = {
url: url.href,
recipient: verifyOptions.to
};
const email = new Email('email-verify', {
url: url.href,
recipient: verifyOptions.to
});
await email.send();
const email = new Email('email-verify', params);
email.send();
cb(null, verifyOptions.to);
cb(null, verifyOptions.to);
} catch (err) {
cb(err);
}
}
}

View File

@ -50,8 +50,8 @@ async function test() {
const JunitReporter = require('jasmine-reporters');
jasmine.addReporter(new JunitReporter.JUnitXmlReporter());
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000;
jasmine.exitOnCompletion = true;
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 900000;
}
const backSpecs = [

View File

@ -1,4 +1,4 @@
FROM mariadb:10.7.7
FROM mariadb:10.11.6
ENV MYSQL_ROOT_PASSWORD root
ENV TZ Europe/Madrid

View File

@ -0,0 +1,81 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`ticketPositionInPath`(vTicketId INT)
RETURNS varchar(10) CHARSET utf8mb3 COLLATE utf8mb3_general_ci
DETERMINISTIC
BEGIN
DECLARE vRestTicketsMaxOrder INT;
DECLARE vRestTicketsMinOrder INT;
DECLARE vRestTicketsPacking INT;
DECLARE vMyProductionOrder INT;
DECLARE vPosition VARCHAR(10) DEFAULT 'MID';
DECLARE vMyPath INT;
DECLARE vMyWarehouse INT;
DECLARE PACKING_ORDER INT;
DECLARE vExpeditionsCount INT;
DECLARE vIsValenciaPath BOOLEAN DEFAULT FALSE;
SELECT `order`
INTO PACKING_ORDER
FROM state
WHERE code = 'PACKING';
SELECT t.routeFk, t.warehouseFk, IFNULL(ts.productionOrder,0)
INTO vMyPath, vMyWarehouse, vMyProductionOrder
FROM ticket t
LEFT JOIN ticketState ts on ts.ticketFk = t.id
WHERE t.id = vTicketId;
SELECT (ag.`name` = 'VN_VALENCIA')
INTO vIsValenciaPath
FROM vn2008.Rutas r
JOIN vn2008.Agencias a on a.Id_Agencia = r.Id_Agencia
JOIN vn2008.agency ag on ag.agency_id = a.agency_id
WHERE r.Id_Ruta = vMyPath;
IF vIsValenciaPath THEN -- Rutas Valencia
SELECT COUNT(*)
INTO vExpeditionsCount
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
WHERE t.routeFk = vMyPath;
SELECT MAX(ts.productionOrder), MIN(ts.productionOrder)
INTO vRestTicketsMaxOrder, vRestTicketsMinOrder
FROM ticket t
LEFT JOIN ticketState ts on t.id = ts.ticketFk
WHERE t.routeFk = vMyPath
AND t.warehouseFk = vMyWarehouse
AND t.id != vTicketid;
SELECT COUNT(*)
INTO vRestTicketsPacking
FROM ticket t
LEFT JOIN ticketState ts on t.id = ts.ticketFk
WHERE ts.productionOrder = PACKING_ORDER
AND t.routeFk = vMyPath
AND t.warehouseFk = vMyWarehouse
AND t.id != vTicketid;
IF vExpeditionsCount = 1 THEN
SET vPosition = 'FIRST';
ELSEIF vRestTicketsMinOrder > PACKING_ORDER THEN
SET vPosition = 'LAST';
ELSEIF vRestTicketsPacking THEN
SET vPosition = 'SHARED';
ELSE
SET vPosition = 'MID';
END IF;
ELSE
SET vPosition = 'MID';
END IF;
RETURN vPosition;
END$$
DELIMITER ;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`expedition_beforeInsert`
BEFORE INSERT ON `expedition`
FOR EACH ROW
BEGIN
DECLARE intcounter INT;
DECLARE vShipFk INT;
SET NEW.editorFk = account.myUser_getId();
IF NEW.freightItemFk IS NOT NULL THEN
UPDATE ticket SET packages = nz(packages) + 1 WHERE id = NEW.ticketFk;
SELECT IFNULL(MAX(counter),0) +1 INTO intcounter
FROM expedition e
INNER JOIN ticket t1 ON e.ticketFk = t1.id
LEFT JOIN ticketState ts ON ts.ticketFk = t1.id
INNER JOIN ticket t2 ON t2.addressFk = t1.addressFk AND DATE(t2.shipped) = DATE(t1.shipped)
AND t1.warehouseFk = t2.warehouseFk
WHERE t2.id = NEW.ticketFk AND ts.alertLevel < 3 AND t1.companyFk = t2.companyFk
AND t1.agencyModeFk = t2.agencyModeFk;
SET NEW.`counter` = intcounter;
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,58 @@
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn`.`expeditionRoute_freeTickets` AS
SELECT
`t`.`routeFk` AS `routeFk`,
`tss`.`ticketFk` AS `ticket`,
`s`.`name` AS `code`,
`w`.`name` AS `almacen`,
`tss`.`updated` AS `updated`,
`p`.`code` AS `parkingCode`
FROM `vn`.`ticketState` `tss`
JOIN `vn`.`ticket` `t` ON `t`.`id` = `tss`.`ticketFk`
JOIN `vn`.`warehouse` `w` ON `w`.`id` = `t`.`warehouseFk`
JOIN `vn`.`state` `s` ON `s`.`id` = `tss`.`state`
LEFT JOIN `vn`.`ticketParking` `tp` ON `tp`.`ticketFk` = `t`.`id`
LEFT JOIN `vn`.`parking` `p` ON `p`.`id` = `tp`.`parkingFk`
WHERE IFNULL(`t`.`packages`, 0) = 0;
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn`.`ticketState`
AS SELECT `tt`.`created` AS `updated`,
`tt`.`stateFk` AS `stateFk`,
`tt`.`userFk` AS `userFk`,
`tls`.`ticketFk` AS `ticketFk`,
`s`.`id` AS `state`,
`s`.`order` AS `productionOrder`,
`s`.`alertLevel` AS `alertLevel`,
`s`.`code` AS `code`,
`s`.`isPreviousPreparable` AS `isPreviousPreparable`,
`s`.`isPicked` AS `isPicked`
FROM (
(
`vn`.`ticketLastState` `tls`
JOIN `vn`.`ticketTracking` `tt` ON(`tt`.`id` = `tls`.`ticketTrackingFk`)
)
JOIN `vn`.`state` `s` ON(`s`.`id` = `tt`.`stateFk`)
);
CREATE OR REPLACE DEFINER=`root`@`localhost`
SQL SECURITY DEFINER
VIEW `vn`.`ticketStateToday`
AS
SELECT
`ts`.`ticketFk` AS `ticket`,
`ts`.`state` AS `state`,
`ts`.`productionOrder` AS `productionOrder`,
`ts`.`alertLevel` AS `alertLevel`,
`ts`.`userFk` AS `worker`,
`ts`.`code` AS `code`,
`ts`.`updated` AS `updated`,
`ts`.`isPicked` AS `isPicked`
FROM
`vn`.`ticketState` `ts`
JOIN `vn`.`ticket` `t` ON `t`.`id` = `ts`.`ticketFk`
WHERE
`t`.`shipped` BETWEEN `util`.`VN_CURDATE`() AND `util`.`VN_CURDATE`() + INTERVAL 1 DAY;

View File

@ -0,0 +1,58 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`invoiceOut_beforeInsert`
BEFORE INSERT ON `invoiceOut`
FOR EACH ROW
BEGIN
/**
* Reference format:
* - 0: Serial [A-Z]
* - 1: Sage company id
* - 2-3: Last two digits of issued year
* - 4-8: Autoincrement identifier
**/
DECLARE vNewRef INT DEFAULT 0;
DECLARE vCompanyCode INT;
DECLARE vLastRef VARCHAR(255);
DECLARE vRefStr VARCHAR(255);
DECLARE vRefLen INT DEFAULT 5;
DECLARE vYearLen INT DEFAULT 2;
DECLARE vPrefixLen INT;
SELECT companyCode INTO vCompanyCode
FROM company
WHERE id = NEW.companyFk;
IF vCompanyCode IS NULL THEN
CALL util.throw('sageCompanyNotDefined');
END IF;
SELECT MAX(i.ref) INTO vLastRef
FROM invoiceOut i
WHERE i.serial = NEW.serial
AND i.issued BETWEEN util.firstDayOfYear(NEW.issued) AND util.dayEnd(util.lastDayOfYear(NEW.issued))
AND i.companyFk = NEW.companyFk;
IF vLastRef IS NOT NULL THEN
SET vPrefixLen = LENGTH(NEW.serial) + LENGTH(vCompanyCode) + vYearLen;
SET vRefLen = LENGTH(vLastRef) - vPrefixLen;
SET vRefStr = SUBSTRING(vLastRef, vPrefixLen + 1);
SET vNewRef = vRefStr + 1;
IF LENGTH(vNewRef) > vRefLen THEN
CALL util.throw('refLenExceeded');
END IF;
SET NEW.ref = CONCAT(
SUBSTRING(vLastRef, 1, vPrefixLen),
LPAD(vNewRef, LENGTH(vRefStr), '0')
);
ELSE
SET NEW.ref = CONCAT(
NEW.serial,
vCompanyCode,
RIGHT(YEAR(NEW.issued), vYearLen),
LPAD(1, vRefLen, '0')
);
END IF;
END$$
DELIMITER ;

View File

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

View File

@ -2911,8 +2911,7 @@ INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk
INSERT INTO `vn`.`ticketRefund`(`refundTicketFk`, `originalTicketFk`)
VALUES
(1, 12),
(8, 10);
(24, 7);
INSERT INTO `vn`.`deviceProductionModels` (`code`)
VALUES
@ -3011,6 +3010,15 @@ INSERT INTO `vn`.`invoiceCorrectionType` (`id`, `description`)
(2, 'Error in sales details'),
(3, 'Error in customer data');
UPDATE `vn`.`client`
SET fi='65004204V'
WHERE id=1;
UPDATE `vn`.`worker`
SET fi='59328808D'
WHERE id=1106;
INSERT INTO `account`.`mailAliasAcl` (`mailAliasFk`, `roleFk`)
VALUES
(1, 1),
@ -3022,16 +3030,16 @@ INSERT INTO `vn`.`docuwareTablet` (`tablet`,`description`)
('Tablet1','Jarvis tablet'),
('Tablet2','Avengers tablet');
INSERT INTO `vn`.`sms` (`id`, `senderFk`, `sender`, `destination`, `message`, `statusCode`, `status`, `created`)
INSERT INTO `vn`.`sms` (`id`, `senderFk`, `sender`, `destination`, `message`, `statusCode`, `status`, `created`)
VALUES (1, 66, '111111111', '0001111111111', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'OK', util.VN_CURDATE()),
(2, 66, '222222222', '0002222222222', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'PENDING', util.VN_CURDATE()),
(3, 66, '333333333', '0003333333333', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'ERROR', util.VN_CURDATE()),
(4, 66, '444444444', '0004444444444', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'OK', util.VN_CURDATE());
INSERT INTO `vn`.`clientSms` (`id`, `clientFk`, `smsFk`, `ticketFk`)
INSERT INTO `vn`.`clientSms` (`id`, `clientFk`, `smsFk`, `ticketFk`)
VALUES(1, 1103, 1, NULL),
(2, 1103, 2, NULL),
(3, 1103, 3, 32),
(4, 1103, 4, 32),
(13, 1101, 1, NULL),
(14, 1101, 4, 27);
(14, 1101, 4, 27);

View File

@ -225,7 +225,7 @@ describe('Ticket Edit sale path', () => {
});
it('should show error trying to delete a ticket with a refund', async() => {
await page.accessToSearchResult('16');
await page.accessToSearchResult('6');
await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket);
await page.waitToClick(selectors.globalItems.acceptButton);

View File

@ -69,8 +69,7 @@
"Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})",
"Change quantity": "{{concept}} change of {{oldQuantity}} to {{newQuantity}}",
"Claim will be picked": "The product from the claim [({{claimId}})]({{{claimUrl}}}) from the client *{{clientName}}* will be picked",
"Claim state has changed to incomplete": "The state of the claim [({{claimId}})]({{{claimUrl}}}) from client *{{clientName}}* has changed to *incomplete*",
"Claim state has changed to canceled": "The state of the claim [({{claimId}})]({{{claimUrl}}}) from client *{{clientName}}* has changed to *canceled*",
"Claim state has changed to": "The state of the claim [({{claimId}})]({{{claimUrl}}}) from client *{{clientName}}* has changed to *{{newState}}*",
"Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member",
"Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member",
"Client checked as validated despite of duplication": "Client checked as validated despite of duplication from client id {{clientId}}",

View File

@ -136,8 +136,7 @@
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
"Change quantity": "{{concept}} cambia de {{oldQuantity}} a {{newQuantity}}",
"Claim will be picked": "Se recogerá el género de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}*",
"Claim state has changed to incomplete": "Se ha cambiado el estado de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}* a *incompleta*",
"Claim state has changed to canceled": "Se ha cambiado el estado de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}* a *anulado*",
"Claim state has changed to": "Se ha cambiado el estado de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}* a *{{newState}}*",
"Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}",
"ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto",
"Distance must be lesser than 1000": "La distancia debe ser inferior a 1000",
@ -332,6 +331,7 @@
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}",
"This claim has been updated": "La reclamación con Id: {{claimId}}, ha sido actualizada",
"This user does not have an assigned tablet": "Este usuario no tiene tablet asignada",
"Incorrect pin": "Pin incorrecto.",
"You already have the mailAlias": "Ya tienes este alias de correo",

View File

@ -7,7 +7,8 @@ const execFile = require('child_process').execFile;
* https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
*/
const UserAccountControlFlags = {
ACCOUNTDISABLE: 2
ACCOUNTDISABLE: 0x2,
DONT_EXPIRE_PASSWD: 0x10000
};
module.exports = Self => {
@ -118,7 +119,8 @@ module.exports = Self => {
}
entry = {
userAccountControl: sambaUser.userAccountControl
userAccountControl: (sambaUser.userAccountControl
| UserAccountControlFlags.DONT_EXPIRE_PASSWD)
& ~UserAccountControlFlags.ACCOUNTDISABLE,
uidNumber: info.uidNumber,
accountExpires: 0,

View File

@ -120,7 +120,7 @@ module.exports = Self => {
observationTypeFk: obsevationType.id
}, myOptions);
await models.TicketTracking.create({
await models.Ticket.state(ctx, {
ticketFk: newRefundTicket.id,
stateFk: state.id,
userFk: worker.id

View File

@ -1,8 +1,9 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
const i18n = require('i18n');
describe('Update Claim', () => {
let url;
let claimStatesMap = {};
beforeAll(async() => {
url = await app.models.Url.getUrl();
const activeCtx = {
@ -16,6 +17,8 @@ describe('Update Claim', () => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const claimStates = await app.models.ClaimState.find();
claimStatesMap = claimStates.reduce((acc, state) => ({...acc, [state.code]: state.id}), {});
});
const newDate = Date.vnNew();
const originalData = {
@ -62,6 +65,123 @@ describe('Update Claim', () => {
expect(error.message).toEqual(`You don't have enough privileges to change that field`);
});
it(`should success to update the claimState to 'pending' and send a rocket message`, async() => {
const tx = await app.models.Claim.beginTransaction({});
try {
const options = {transaction: tx};
const newClaim = await app.models.Claim.create(originalData, options);
const chatModel = app.models.Chat;
spyOn(chatModel, 'sendCheckingPresence').and.callThrough();
const pendingState = claimStatesMap.pending;
const claimManagerId = 72;
const ctx = {
req: {
accessToken: {userId: claimManagerId},
headers: {origin: url}
},
args: {
observation: 'valid observation',
claimStateFk: pendingState,
hasToPickUp: false
}
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);
expect(updatedClaim.observation).toEqual(ctx.args.observation);
expect(chatModel.sendCheckingPresence).toHaveBeenCalled();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it(`should success to update the claimState to 'managed' and send a rocket message`, async() => {
const tx = await app.models.Claim.beginTransaction({});
try {
const options = {transaction: tx};
const newClaim = await app.models.Claim.create(originalData, options);
const chatModel = app.models.Chat;
spyOn(chatModel, 'sendCheckingPresence').and.callThrough();
const managedState = claimStatesMap.managed;
const claimManagerId = 72;
const ctx = {
req: {
accessToken: {userId: claimManagerId},
headers: {origin: url}
},
args: {
observation: 'valid observation',
claimStateFk: managedState,
hasToPickUp: false
}
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);
expect(updatedClaim.observation).toEqual(ctx.args.observation);
expect(chatModel.sendCheckingPresence).toHaveBeenCalled();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it(`should success to update the claimState to 'resolved' and send a rocket message`, async() => {
const tx = await app.models.Claim.beginTransaction({});
try {
const options = {transaction: tx};
const newClaim = await app.models.Claim.create(originalData, options);
const chatModel = app.models.Chat;
spyOn(chatModel, 'sendCheckingPresence').and.callThrough();
const resolvedState = claimStatesMap.resolved;
const claimManagerId = 72;
const ctx = {
req: {
accessToken: {userId: claimManagerId},
headers: {origin: url}
},
args: {
observation: 'valid observation',
claimStateFk: resolvedState,
hasToPickUp: false
}
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);
expect(updatedClaim.observation).toEqual(ctx.args.observation);
expect(chatModel.sendCheckingPresence).toHaveBeenCalled();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it(`should success to update the claimState to 'canceled' and send a rocket message`, async() => {
const tx = await app.models.Claim.beginTransaction({});
@ -73,7 +193,7 @@ describe('Update Claim', () => {
const chatModel = app.models.Chat;
spyOn(chatModel, 'sendCheckingPresence').and.callThrough();
const canceledState = 4;
const canceledState = claimStatesMap.canceled;
const claimManagerId = 72;
const ctx = {
req: {
@ -86,9 +206,7 @@ describe('Update Claim', () => {
hasToPickUp: false
}
};
ctx.req.__ = (value, params) => {
return params.nickname;
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);
@ -127,9 +245,7 @@ describe('Update Claim', () => {
hasToPickUp: false
}
};
ctx.req.__ = (value, params) => {
return params.nickname;
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);
@ -168,9 +284,7 @@ describe('Update Claim', () => {
hasToPickUp: true
}
};
ctx.req.__ = (value, params) => {
return params.nickname;
};
ctx.req.__ = i18n.__;
await app.models.Claim.updateClaim(ctx, newClaim.id, options);
let updatedClaim = await app.models.Claim.findById(newClaim.id, null, options);

View File

@ -96,12 +96,9 @@ module.exports = Self => {
// When claimState has been changed
if (args.claimStateFk) {
const newState = await models.ClaimState.findById(args.claimStateFk, null, myOptions);
if (newState.hasToNotify) {
if (newState.code == 'incomplete')
await notifyStateChange(ctx, salesPerson.id, claim, newState.code);
if (newState.code == 'canceled')
await notifyStateChange(ctx, claim.workerFk, claim, newState.code);
}
await notifyStateChange(ctx, salesPerson.id, claim, newState.code);
if (newState.code == 'canceled')
await notifyStateChange(ctx, claim.workerFk, claim, newState.code);
}
if (tx) await tx.commit();
@ -113,15 +110,16 @@ module.exports = Self => {
}
};
async function notifyStateChange(ctx, workerId, claim, state) {
async function notifyStateChange(ctx, workerId, claim, newState) {
const models = Self.app.models;
const url = await models.Url.getUrl();
const $t = ctx.req.__; // $translate
const message = $t(`Claim state has changed to ${state}`, {
const message = $t(`Claim state has changed to`, {
claimId: claim.id,
clientName: claim.client().name,
claimUrl: `${url}claim/${claim.id}/summary`
claimUrl: `${url}claim/${claim.id}/summary`,
newState
});
await models.Chat.sendCheckingPresence(ctx, workerId, message);
}

View File

@ -27,4 +27,4 @@
label="Cancel">
</vn-button>
</vn-button-bar>
</form>
</form>

View File

@ -78,7 +78,7 @@ module.exports = Self => {
const sales = await models.Sale.find(filterTicket, myOptions);
const salesIds = sales.map(sale => sale.id);
const clonedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, false, myOptions);
const clonedTickets = await models.Sale.clone(ctx, salesIds, servicesIds, null, false, myOptions);
const clonedTicketIds = [];
for (const clonedTicket of clonedTickets) {

View File

@ -1,5 +1,5 @@
module.exports = Self => {
Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, group, negative, options) => {
Self.clone = async(ctx, salesIds, servicesIds, withWarehouse, negative, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
@ -28,8 +28,6 @@ module.exports = Self => {
const mappedTickets = new Map();
if (group) ticketsIds = [ticketsIds[0]];
for (let ticketId of ticketsIds) {
const newTicket = await createTicket(
ctx,
@ -109,7 +107,10 @@ module.exports = Self => {
const newTicket = await models.Ticket.new(ctx, myOptions);
if (negative) {
const ticketRefund = await models.TicketRefund.findOne({
where: {refundTicketFk: ticketId}
}, myOptions);
if (negative && (withWarehouse || !ticketRefund?.id)) {
await models.TicketRefund.create({
originalTicketFk: ticketId,
refundTicketFk: newTicket.id

View File

@ -47,7 +47,6 @@ module.exports = Self => {
salesIds,
servicesIds,
withWarehouse,
false,
true,
myOptions
);

View File

@ -0,0 +1,70 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Ticket cloning - clone function', () => {
let ctx;
let options;
let tx;
beforeEach(async() => {
ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'}
},
args: {}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: ctx.req
});
options = {transaction: tx};
tx = await models.Sale.beginTransaction({});
options.transaction = tx;
});
afterEach(async() => {
await tx.rollback();
});
it('should create new tickets with cloned sales with warehouse', async() => {
const salesIds = [1, 2, 3];
const servicesIds = [];
const withWarehouse = true;
const negative = false;
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, withWarehouse, negative, options);
expect(newTickets).toBeDefined();
expect(newTickets.length).toBeGreaterThan(0);
});
it('should handle negative quantities correctly', async() => {
const negative = true;
const salesIds = [7, 8];
const servicesIds = [];
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, false, negative, options);
for (const ticket of newTickets) {
const sales = await models.Sale.find({where: {ticketFk: ticket.id}}, options);
sales.forEach(sale => {
expect(sale.quantity).toBeLessThan(0);
});
}
});
it('should create new components and services for cloned tickets', async() => {
const servicesIds = [2];
const salesIds = [5];
const newTickets = await models.Sale.clone(ctx, salesIds, servicesIds, false, false, options);
for (const ticket of newTickets) {
const sale = await models.Sale.findOne({where: {ticketFk: ticket.id}}, options);
const components = await models.SaleComponent.find({where: {saleFk: sale.id}}, options);
const services = await models.TicketService.find({where: {ticketFk: ticket.id}}, options);
expect(components.length).toBeGreaterThan(0);
expect(services.length).toBeGreaterThan(0);
}
});
});

View File

@ -49,7 +49,7 @@ module.exports = Self => {
for (const id of ticketIds) {
const promise = await models.Ticket.state(ctx, {
stateFk: state.id,
workerFk: worker.id,
userFk: worker.id,
ticketFk: id
}, myOptions);
promises.push(promise);

View File

@ -84,7 +84,7 @@ module.exports = Self => {
arg: 'filter',
type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}
},
],
returns: {
type: ['object'],

View File

@ -130,7 +130,17 @@ module.exports = Self => {
await models.TicketDms.create({ticketFk: ticketId, dmsFk: dms[0].id}, myOptions);
const ticket = await models.Ticket.findById(ticketId, null, myOptions);
await ticket.updateAttribute('isSigned', true, myOptions);
await Self.rawSql(`CALL vn.ticket_setState(?, ?)`, [ticketId, 'DELIVERED'], myOptions);
const deliveryState = await models.State.find({
where: {
code: 'DELIVERED'
}
}, options);
await models.Ticket.state(ctx, {
ticketFk: ticketId,
stateFk: deliveryState.id
}, myOptions);
}
if (tx) await tx.commit();

View File

@ -3,7 +3,6 @@ const LoopBackContext = require('loopback-context');
describe('ticket setDeleted()', () => {
const userId = 1106;
const employeeUser = 1110;
const activeCtx = {
accessToken: {userId: userId},
};
@ -118,7 +117,7 @@ describe('ticket setDeleted()', () => {
return value;
};
const ticketId = 12;
const ticketId = 7;
await models.Ticket.setDeleted(ctx, ticketId, options);
await tx.rollback();

View File

@ -45,9 +45,8 @@ describe('ticket state()', () => {
const options = {transaction: tx};
activeCtx.accessToken.userId = salesPersonId;
const params = {ticketFk: 2, stateFk: 3};
await models.Ticket.state(ctx, params, options);
await models.Ticket.state(ctx, {ticketFk: 2, stateFk: 3}, options);
await tx.rollback();
} catch (e) {
@ -67,9 +66,8 @@ describe('ticket state()', () => {
const options = {transaction: tx};
activeCtx.accessToken.userId = employeeId;
const params = {ticketFk: 11, stateFk: 13};
await models.Ticket.state(ctx, params, options);
await models.Ticket.state(ctx, {ticketFk: 11, stateFk: 13}, options);
await tx.rollback();
} catch (e) {

View File

@ -7,7 +7,6 @@ module.exports = Self => {
accepts: [
{
arg: 'data',
description: 'Model instance data',
type: 'Object',
required: true,
http: {source: 'body'}
@ -37,25 +36,21 @@ module.exports = Self => {
}
try {
const userId = ctx.req.accessToken.userId;
if (!params.stateFk && !params.code)
throw new UserError('State cannot be blank');
if (params.code) {
const state = await models.State.findOne({
where: {code: params.code},
fields: ['id']
}, myOptions);
params.stateFk = state.id;
if (params.stateFk) {
const {code} = await models.State.findById(params.stateFk, {fields: ['code']}, myOptions);
params.code = code;
} else {
const {id} = await models.State.findOne({where: {code: params.code}}, myOptions);
params.stateFk = id;
}
if (!params.userFk) {
const worker = await models.Worker.findOne({
where: {id: userId}
where: {id: ctx.req.accessToken.userId}
}, myOptions);
params.userFk = worker.id;
}
@ -63,17 +58,21 @@ module.exports = Self => {
fields: ['stateFk']
}, myOptions);
let oldStateAllowed;
if (ticketState)
oldStateAllowed = await models.State.isEditable(ctx, ticketState.stateFk, myOptions);
const oldStateAllowed = ticketState && await models.State.isEditable(ctx, ticketState.stateFk, myOptions);
const newStateAllowed = await models.State.isEditable(ctx, params.stateFk, myOptions);
const isAllowed = (!ticketState || oldStateAllowed == true) && newStateAllowed == true;
if (!isAllowed)
if ((ticketState && !oldStateAllowed) || !newStateAllowed)
throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
const ticketTracking = await models.TicketTracking.create(params, myOptions);
await Self.rawSql(`CALL vn.ticket_setState(?, ?)`, [params.ticketFk, params.code], myOptions);
const ticketTracking = await models.TicketTracking.findOne({
where: {ticketFk: params.ticketFk},
order: 'id DESC',
limit: 1
}, myOptions);
await ticketTracking.updateAttribute('userFk', params.userFk, myOptions);
if (tx) await tx.commit();

View File

@ -132,7 +132,7 @@ describe('sale model ', () => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue(getActiveCtx(9));
const tx = await models.Sale.beginTransaction({});
const saleId = 13;
const saleId = 32;
const newQuantity = -10;
try {

View File

@ -36,7 +36,7 @@
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "workerFk"
"foreignKey": "userFk"
}
}
}

View File

@ -61,6 +61,9 @@
<th field="liters">
<span translate>Liters</span>
</th>
<th field="totalWithVat">
<span translate>Import</span>
</th>
<th shrink field="lines">
<span translate>Available Lines</span>
</th>
@ -76,6 +79,7 @@
<th shrink field="futureState">
<span translate>State</span>
</th>
</tr>
</thead>
<tbody>
@ -148,6 +152,13 @@
</span>
</td>
<td>{{::ticket.liters}}</td>
<td>
<span
class="chip {{$ctrl.totalPriceColor(ticket.totalWithVat)}}"
title="{{$ctrl.totalPriceTitle(ticket.totalWithVat) | translate}}">
{{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
</span>
</td>
<td>{{::ticket.lines}}</td>
<td separator>
<span

View File

@ -21,6 +21,9 @@ export default class Controller extends Section {
{
field: 'futureShipped',
searchable: false
}, {
field: 'totalWithVat',
searchable: false
},
{
field: 'state',
@ -130,6 +133,17 @@ export default class Controller extends Section {
this.vnApp.showSuccess(this.$t('Success'));
});
}
totalPriceColor(totalWithVat) {
return this.isLessThan50(totalWithVat) ? 'warning' : '';
}
totalPriceTitle(totalWithVat) {
return this.isLessThan50(totalWithVat) ? 'Less than 50€' : '';
}
isLessThan50(totalWithVat) {
return (parseInt(totalWithVat) > 0 && parseInt(totalWithVat) < 50);
}
exprBuilder(param, value) {
switch (param) {
@ -145,6 +159,8 @@ export default class Controller extends Section {
return {'ipt': {like: `%${value}%`}};
case 'futureIpt':
return {'futureIpt': {like: `%${value}%`}};
case 'totalWithVat':
return {'totalWithVat': value};
}
}
}

View File

@ -22,9 +22,9 @@
<vn-tr ng-repeat="tracking in trackings">
<vn-td>{{::tracking.state.name}}</vn-td>
<vn-td expand>
<span
ng-class="{'link': tracking.user.id}"
ng-click="workerDescriptor.show($event, tracking.user.id)">
<span
ng-class="{'link': tracking.user.worker}"
ng-click="tracking.user.worker && workerDescriptor.show($event, tracking.user.worker.id)">
{{::tracking.user.name || 'System' | translate}}
</span>
</vn-td>

View File

@ -9,7 +9,13 @@ class Controller extends Section {
{
relation: 'user',
scope: {
fields: ['name']
fields: ['id', 'name'],
include: {
relation: 'worker',
scope: {
fields: ['id']
}
}
}
}, {
relation: 'state',

View File

@ -5,7 +5,7 @@ module.exports = Self => {
accepts: [
{
arg: 'workerFk',
type: 'int',
type: 'number',
required: true,
},

View File

@ -1,4 +1,5 @@
module.exports = Self => {
const validateTin = require('vn-loopback/util/validateTin');
require('../methods/worker/filter')(Self);
require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self);
@ -23,4 +24,21 @@ module.exports = Self => {
Self.validatesUniquenessOf('locker', {
message: 'This locker has already been assigned'
});
Self.validateAsync('fi', tinIsValid, {
message: 'Invalid TIN'
});
async function tinIsValid(err, done) {
const filter = {
fields: ['code'],
where: {id: this.countryFk}
};
const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null;
if (!this.fi || !validateTin(this.fi, code))
err();
done();
}
};

View File

@ -61,7 +61,7 @@ module.exports = Self => {
for (ticket of ticketList) {
if (ticket.ticketState().alertLevel == 0) {
promises.push(models.TicketTracking.create({
promises.push(models.Ticket.state(ctx, {
ticketFk: ticket.id,
stateFk: fixingState.id,
userFk: worker.id