Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2502-supplier_summary
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Bernat Exposito Domenech 2020-10-26 08:43:20 +01:00
commit 1699e0619c
32 changed files with 633 additions and 81 deletions

View File

@ -23,21 +23,23 @@ module.exports = Self => {
} }
}); });
Self.sendCheckingPresence = async(ctx, workerId, message) => { Self.sendCheckingPresence = async(ctx, recipientId, message) => {
if (!workerId) return false; if (!recipientId) return false;
const models = Self.app.models; const models = Self.app.models;
const account = await models.Account.findById(workerId); const account = await models.Account.findById(recipientId);
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
if (recipientId == userId) return false;
if (!account) if (!account)
throw new Error(`Could not send message "${message}" to worker id ${workerId} from user ${userId}`); throw new Error(`Could not send message "${message}" to worker id ${recipientId} from user ${userId}`);
const query = `SELECT worker_isWorking(?) isWorking`; const query = `SELECT worker_isWorking(?) isWorking`;
const [result] = await Self.rawSql(query, [workerId]); const [result] = await Self.rawSql(query, [recipientId]);
if (!result.isWorking) { if (!result.isWorking) {
const workerDepartment = await models.WorkerDepartment.findById(workerId, { const workerDepartment = await models.WorkerDepartment.findById(recipientId, {
include: { include: {
relation: 'department' relation: 'department'
} }
@ -46,7 +48,7 @@ module.exports = Self => {
const channelName = department && department.chatName; const channelName = department && department.chatName;
if (channelName) if (channelName)
return Self.send(ctx, `#${channelName}`, `@${account.name} => ${message}`); return Self.send(ctx, `#${channelName}`, `@${account.name} ${message}`);
} }
return Self.send(ctx, `@${account.name}`, message); return Self.send(ctx, `@${account.name}`, message);

View File

@ -38,7 +38,7 @@ describe('chat sendCheckingPresence()', () => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent'); expect(response.message).toEqual('Fake notification sent');
expect(chatModel.send).toHaveBeenCalledWith(ctx, '#cooler', '@HankPym => I changed something'); expect(chatModel.send).toHaveBeenCalledWith(ctx, '#cooler', '@HankPym I changed something');
// restores // restores
await department.updateAttribute('chatName', null); await department.updateAttribute('chatName', null);

View File

@ -0,0 +1,107 @@
USE `vn`;
DROP procedure IF EXISTS `ticket_componentPreview`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` PROCEDURE `ticket_componentPreview`(
vTicketFk INT,
vLanded DATE,
vAddressFk INT,
vZoneFk INT,
vWarehouseFk SMALLINT)
BEGIN
/**
* Calcula los componentes de los articulos de un ticket
*
* @param vTicketFk id del ticket
* @param vLanded nueva fecha de entrega
* @param vAddressFk nuevo consignatario
* @param vZoneFk nueva zona
* @param vWarehouseFk nuevo warehouse
*
* @return tmp.ticketComponentPreview (warehouseFk, itemFk, componentFk, cost)
*/
DECLARE vHasDataChanged BOOL DEFAULT FALSE;
DECLARE vHasAddressChanged BOOL;
DECLARE vHasZoneChanged BOOL DEFAULT FALSE;
DECLARE vHasWarehouseChanged BOOL DEFAULT FALSE;
DECLARE vShipped DATE;
DECLARE vAddressTypeRateFk INT DEFAULT NULL;
DECLARE vAgencyModeTypeRateFk INT DEFAULT NULL;
DECLARE vHasChangeAll BOOL DEFAULT FALSE;
SELECT DATE(landed) <> vLanded,
addressFk <> vAddressFk,
zoneFk <> vZoneFk,
warehouseFk <> vWarehouseFk
INTO
vHasDataChanged,
vHasAddressChanged,
vHasZoneChanged,
vHasWarehouseChanged
FROM vn.ticket t
WHERE t.id = vTicketFk;
IF vHasDataChanged OR vHasWarehouseChanged THEN
SET vHasChangeAll = TRUE;
END IF;
IF vHasAddressChanged THEN
SET vAddressTypeRateFk = 5;
END IF;
IF vHasZoneChanged THEN
SET vAgencyModeTypeRateFk = 6;
END IF;
SELECT TIMESTAMPADD(DAY, -travelingDays, vLanded) INTO vShipped
FROM zone
WHERE id = vZoneFk;
CALL buyUltimate(vWarehouseFk, vShipped);
DROP TEMPORARY TABLE IF EXISTS tmp.ticketLot;
CREATE TEMPORARY TABLE tmp.ticketLot ENGINE = MEMORY (
SELECT
vWarehouseFk AS warehouseFk,
NULL AS available,
s.itemFk,
bu.buyFk,
vZoneFk zoneFk
FROM sale s
LEFT JOIN tmp.buyUltimate bu ON bu.itemFk = s.itemFk
WHERE s.ticketFk = vTicketFk
GROUP BY bu.warehouseFk, bu.itemFk);
CALL catalog_componentPrepare();
CALL catalog_componentCalculate(vZoneFk, vAddressFk, vShipped, vWarehouseFk);
REPLACE INTO tmp.ticketComponent (warehouseFk, itemFk, componentFk, cost)
SELECT t.warehouseFk, s.itemFk, sc.componentFk, sc.value
FROM saleComponent sc
JOIN sale s ON s.id = sc.saleFk
JOIN ticket t ON t.id = s.ticketFk
JOIN `component` c ON c.id = sc.componentFk
WHERE s.ticketFk = vTicketFk
AND (c.isRenewable = FALSE
OR
(NOT vHasChangeAll
AND (NOT (c.typeFk <=> vAddressTypeRateFk
OR c.typeFk <=> vAgencyModeTypeRateFk))));
DROP TEMPORARY TABLE IF EXISTS tmp.ticketComponentPreview;
CREATE TEMPORARY TABLE tmp.ticketComponentPreview
SELECT * FROM tmp.ticketComponent;
CALL catalog_componentPurge();
DROP TEMPORARY TABLE tmp.buyUltimate;
IF vShipped IS NULL THEN
CALL util.throw('NO_ZONE_AVAILABLE');
END IF;
END$$
DELIMITER ;

View File

@ -399,16 +399,16 @@ let actions = {
pickDate: async function(selector, date) { pickDate: async function(selector, date) {
date = date || new Date(); date = date || new Date();
const tzoffset = date.getTimezoneOffset() * 60000; const timeZoneOffset = date.getTimezoneOffset() * 60000;
const localIso = (new Date(date.getTime() - tzoffset)) const localDate = (new Date(date.getTime() - timeZoneOffset))
.toISOString(); .toISOString().substr(0, 10);
await this.wait(selector); await this.wait(selector);
await this.evaluate((selector, localIso) => { await this.evaluate((selector, localDate) => {
let input = document.querySelector(selector).$ctrl.input; let input = document.querySelector(selector).$ctrl.input;
input.value = localIso.substr(0, 10); input.value = localDate;
input.dispatchEvent(new Event('change')); input.dispatchEvent(new Event('change'));
}, selector, localIso); }, selector, localDate);
}, },
pickTime: async function(selector, time) { pickTime: async function(selector, time) {

View File

@ -83,6 +83,9 @@ export default {
equalizationTaxCheckbox: 'vn-client-fiscal-data vn-check[ng-model="$ctrl.client.isEqualizated"]', equalizationTaxCheckbox: 'vn-client-fiscal-data vn-check[ng-model="$ctrl.client.isEqualizated"]',
address: 'vn-client-fiscal-data vn-textfield[ng-model="$ctrl.client.street"]', address: 'vn-client-fiscal-data vn-textfield[ng-model="$ctrl.client.street"]',
postcode: 'vn-client-fiscal-data vn-datalist[ng-model="$ctrl.client.postcode"]', postcode: 'vn-client-fiscal-data vn-datalist[ng-model="$ctrl.client.postcode"]',
sageTax: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.sageTaxTypeFk"]',
sageTransaction: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.sageTransactionTypeFk"]',
transferor: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.transferorFk"]',
city: 'vn-client-fiscal-data vn-datalist[ng-model="$ctrl.client.city"]', city: 'vn-client-fiscal-data vn-datalist[ng-model="$ctrl.client.city"]',
province: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.provinceFk"]', province: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.provinceFk"]',
country: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.countryFk"]', country: 'vn-client-fiscal-data vn-autocomplete[ng-model="$ctrl.client.countryFk"]',
@ -199,6 +202,21 @@ export default {
firstDocWorker: 'vn-client-dms-index vn-td:nth-child(8) > span', firstDocWorker: 'vn-client-dms-index vn-td:nth-child(8) > span',
firstDocWorkerDescriptor: '.vn-popover.shown vn-worker-descriptor' firstDocWorkerDescriptor: '.vn-popover.shown vn-worker-descriptor'
}, },
clientCreditInsurance: {
addNewContract: 'vn-client-credit-insurance-index vn-float-button[ui-sref="client.card.creditInsurance.create"]',
newCreditClassification: 'vn-client-credit-insurance-create vn-input-number[ng-model="$ctrl.creditClassification.credit"]',
newInsuranceCredit: 'vn-client-credit-insurance-insurance-create vn-input-number[ng-model="$ctrl.insurance.credit"]',
newClassificationGrade: 'vn-client-credit-insurance-create vn-input-number[ng-model="$ctrl.creditClassification.grade"]',
newInsuranceGrade: 'vn-client-credit-insurance-insurance-create vn-input-number[ng-model="$ctrl.insurance.grade"]',
newClassificationStartingDate: 'vn-client-credit-insurance-create vn-date-picker[ng-model="$ctrl.creditClassification.started"]',
newInsuranceStartingDate: 'vn-client-credit-insurance-insurance-create vn-date-picker[ng-model="$ctrl.insurance.created"]',
endCurrentContract: 'vn-client-credit-insurance-index vn-icon-button[icon="lock"]',
firstContratViewCreditButton: 'vn-client-credit-insurance-index vn-card > vn-horizontal:nth-child(1) vn-icon-button[icon="desktop_windows"]',
addNewCredit: 'vn-client-credit-insurance-insurance-index vn-float-button vn-icon[icon="add"]',
saveNewContract: 'vn-client-credit-insurance-create vn-submit',
saveNewInsuranceCredit: 'vn-client-credit-insurance-insurance-create button[type="submit"]',
anyCreditInsuranceLine: 'vn-client-credit-insurance-insurance-index vn-tbody > vn-tr',
},
clientContacts: { clientContacts: {
addContactButton: 'vn-client-contact vn-icon[icon="add_circle"]', addContactButton: 'vn-client-contact vn-icon[icon="add_circle"]',
name: 'vn-client-contact vn-textfield[ng-model="contact.name"]', name: 'vn-client-contact vn-textfield[ng-model="contact.name"]',

View File

@ -66,6 +66,9 @@ describe('Client Edit fiscalData path', () => {
await page.autocompleteSearch(selectors.clientFiscalData.province, 'Province one'); await page.autocompleteSearch(selectors.clientFiscalData.province, 'Province one');
await page.clearInput(selectors.clientFiscalData.city); await page.clearInput(selectors.clientFiscalData.city);
await page.write(selectors.clientFiscalData.city, 'Valencia'); await page.write(selectors.clientFiscalData.city, 'Valencia');
await page.autocompleteSearch(selectors.clientFiscalData.sageTax, 'operaciones no sujetas');
await page.autocompleteSearch(selectors.clientFiscalData.sageTransaction, 'regularización de inversiones');
await page.autocompleteSearch(selectors.clientFiscalData.transferor, 'Max Eisenhardt');
await page.clearInput(selectors.clientFiscalData.postcode); await page.clearInput(selectors.clientFiscalData.postcode);
await page.write(selectors.clientFiscalData.postcode, '46000'); await page.write(selectors.clientFiscalData.postcode, '46000');
await page.waitToClick(selectors.clientFiscalData.activeCheckbox); await page.waitToClick(selectors.clientFiscalData.activeCheckbox);
@ -188,6 +191,24 @@ describe('Client Edit fiscalData path', () => {
expect(result).toContain('46000'); expect(result).toContain('46000');
}); });
it('should confirm the sageTax have been edited', async() => {
const result = await page.waitToGetProperty(selectors.clientFiscalData.sageTax, 'value');
expect(result).toEqual('Operaciones no sujetas');
});
it('should confirm the sageTransaction have been edited', async() => {
const result = await page.waitToGetProperty(selectors.clientFiscalData.sageTransaction, 'value');
expect(result).toEqual('Regularización de inversiones');
});
it('should confirm the transferor have been edited', async() => {
const result = await page.waitToGetProperty(selectors.clientFiscalData.transferor, 'value');
expect(result).toEqual('Max Eisenhardt');
});
it('should confirm the city have been autocompleted', async() => { it('should confirm the city have been autocompleted', async() => {
const result = await page.waitToGetProperty(selectors.clientFiscalData.city, 'value'); const result = await page.waitToGetProperty(selectors.clientFiscalData.city, 'value');

View File

@ -0,0 +1,93 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Client credit insurance path', () => {
let browser;
let page;
let previousMonth = new Date();
previousMonth.setMonth(previousMonth.getMonth() - 1);
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('insurance', 'client');
await page.accessToSearchResult('Tony Stark');
await page.accessToSection('client.card.creditInsurance.index');
});
afterAll(async() => {
await browser.close();
});
it('should open the create a new credit contract form', async() => {
await page.waitToClick(selectors.clientCreditInsurance.addNewContract);
await page.waitForState('client.card.creditInsurance.create');
});
it('should create a new credit contract', async() => {
await page.write(selectors.clientCreditInsurance.newCreditClassification, '1000');
await page.write(selectors.clientCreditInsurance.newClassificationGrade, '1');
await page.pickDate(selectors.clientCreditInsurance.newClassificationStartingDate, previousMonth);
await page.waitToClick(selectors.clientCreditInsurance.saveNewContract);
await page.waitForState('client.card.creditInsurance.index');
});
it(`should verify the addNewContract button is not present since there's an active contract`, async() => {
await page.waitForSelector(selectors.clientCreditInsurance.addNewContract, {hidden: true});
});
it(`should click the view credits button`, async() => {
await page.waitToClick(selectors.clientCreditInsurance.firstContratViewCreditButton);
await page.waitForState('client.card.creditInsurance.insurance.index');
});
it('should click the add new credit button which opens the new credit form', async() => {
await page.waitToClick(selectors.clientCreditInsurance.addNewCredit);
await page.waitForState('client.card.creditInsurance.insurance.create');
});
it('should fill the form but provide no grade to the new credit hence fail', async() => {
await page.write(selectors.clientCreditInsurance.newInsuranceCredit, '2000');
await page.pickDate(selectors.clientCreditInsurance.newInsuranceStartingDate, previousMonth);
await page.waitToClick(selectors.clientCreditInsurance.saveNewInsuranceCredit);
const message = await page.waitForSnackbar();
expect(message.text).toEqual('The grade must be similar to the last one');
});
it('should provide a correct grade and succesfully save a new credit', async() => {
await page.write(selectors.clientCreditInsurance.newInsuranceGrade, '999');
await page.waitToClick(selectors.clientCreditInsurance.saveNewInsuranceCredit);
const message = await page.waitForSnackbar();
expect(message.text).toEqual('Data saved!');
});
it('should be redirected to the credit index', async() => {
await page.waitForState('client.card.creditInsurance.insurance.index');
});
it('should check the amount of credits is the expected', async() => {
const result = await page.countElement(selectors.clientCreditInsurance.anyCreditInsuranceLine);
expect(result).toEqual(2);
});
it('should navigate to the credit insurance section', async() => {
await page.waitToClick(`vn-left-menu li > a[ui-sref="client.card.creditInsurance.index"]`);
await page.waitForState('client.card.creditInsurance.index');
});
it('should bring the current contract to an end', async() => {
await page.waitToClick(selectors.clientCreditInsurance.endCurrentContract);
await page.waitToClick(selectors.globalItems.acceptButton);
});
it(`should verify the addNewContract button is now present since there's no active contract`, async() => {
await page.waitForSelector(selectors.clientCreditInsurance.addNewContract, {visible: true});
});
it(`should verify the endCurrentContract button is not present since there's no active contract`, async() => {
await page.waitForSelector(selectors.clientCreditInsurance.endCurrentContract, {hidden: true});
});
});

View File

@ -0,0 +1,70 @@
import ngModule from '../module';
export function stringifyParams(data) {
const params = Object.assign({}, data.params);
for (let param in params)
params[param] = JSON.stringify(params[param]);
return params;
}
export function changeState($state, event, data) {
const params = stringifyParams(data);
$state.go(data.state, params);
event.preventDefault();
event.stopPropagation();
}
export function openNewTab($state, $window, event, data) {
const params = stringifyParams(data);
const href = $state.href(data.state, params);
$window.open(href);
event.preventDefault();
event.stopPropagation();
}
/**
* Allows changing state for nested anchor
*
* @param {Object} $state
* @param {Object} $window
* @return {Object} The directive
*/
export function directive($state, $window) {
let ctrlPressed = false;
$window.addEventListener('keydown', event => {
if (event.key == 'Control')
ctrlPressed = true;
});
$window.addEventListener('keyup', event => {
if (event.key == 'Control')
ctrlPressed = false;
});
return {
restrict: 'A',
link: function($scope, $element, $attrs) {
const data = $scope.$eval($attrs.vnAnchor);
$element.on('click', event => {
if (ctrlPressed)
openNewTab($state, $window, event, data);
else
changeState($state, event, data);
});
$element.on('mousedown', event => {
const mouseWheel = 1;
if (event.button == mouseWheel)
openNewTab($state, $window, event, data);
});
}
};
}
directive.$inject = ['$state', '$window'];
ngModule.directive('vnAnchor', directive);

View File

@ -15,3 +15,4 @@ import './smart-table';
import './droppable'; import './droppable';
import './http-click'; import './http-click';
import './http-submit'; import './http-submit';
import './anchor';

View File

@ -72,5 +72,13 @@
"Deleted absence": "The worker <strong>{{author}}</strong> has deleted an absence of type '{{absenceType}}' to <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> for day {{dated}}.", "Deleted absence": "The worker <strong>{{author}}</strong> has deleted an absence of type '{{absenceType}}' to <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> for day {{dated}}.",
"I have deleted the ticket id": "I have deleted the ticket id [{{id}}]({{{url}}})", "I have deleted the ticket id": "I have deleted the ticket id [{{id}}]({{{url}}})",
"I have restored the ticket id": "I have restored the ticket id [{{id}}]({{{url}}})", "I have restored the ticket id": "I have restored the ticket id [{{id}}]({{{url}}})",
"Changed this data from the ticket": "I have changed the data from the ticket [{{ticketId}}]({{{ticketUrl}}}): ```{{{changes}}}```" "Changed this data from the ticket": "I have changed the data from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"agencyModeFk": "Agency",
"clientFk": "Client",
"zoneFk": "Zone",
"warehouseFk": "Warehouse",
"shipped": "Shipped",
"landed": "Landed",
"addressFk": "Address",
"companyFk": "Company"
} }

View File

@ -121,11 +121,11 @@
"Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío",
"Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios",
"Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios",
"MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} (#{{itemId}}) para el ticket id [{{ticketId}}]({{{url}}})", "MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} ({{itemId}}) para el ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} (#{{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} (#{{clientId}})]({{{url}}})", "MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} (#{{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
"Claim will be picked": "Se recogerá el género de la reclamación (#{{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}*", "Claim will be picked": "Se recogerá el género de la reclamación ({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}*",
"This ticket is not an stowaway anymore": "El ticket id [{{ticketId}}]({{{ticketUrl}}}) ha dejado de ser un polizón", "This ticket is not an stowaway anymore": "El ticket id [{{ticketId}}]({{{ticketUrl}}}) ha dejado de ser un polizón",
"Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}", "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", "ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto",
@ -140,7 +140,6 @@
"Role already assigned": "Role already assigned", "Role already assigned": "Role already assigned",
"Invalid role name": "Invalid role name", "Invalid role name": "Invalid role name",
"Role name must be written in camelCase": "Role name must be written in camelCase", "Role name must be written in camelCase": "Role name must be written in camelCase",
"can't be set": "can't be set",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",
"User already exists": "User already exists", "User already exists": "User already exists",
"Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral",
@ -149,5 +148,13 @@
"I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})", "I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})",
"I have restored the ticket id": "He restaurado el ticket id [{{id}}]({{{url}}})", "I have restored the ticket id": "He restaurado el ticket id [{{id}}]({{{url}}})",
"You can only restore a ticket within the first hour after deletion": "Únicamente puedes restaurar el ticket dentro de la primera hora después de su eliminación", "You can only restore a ticket within the first hour after deletion": "Únicamente puedes restaurar el ticket dentro de la primera hora después de su eliminación",
"Changed this data from the ticket": "He cambiado estos datos del ticket [{{ticketId}}]({{{ticketUrl}}}): ```{{{changes}}}```" "Changed this data from the ticket": "He cambiado estos datos del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"agencyModeFk": "Agencia",
"clientFk": "Cliente",
"zoneFk": "Zona",
"warehouseFk": "Almacén",
"shipped": "F. envío",
"landed": "F. entrega",
"addressFk": "Consignatario",
"companyFk": "Empresa"
} }

83
loopback/util/log.js Normal file
View File

@ -0,0 +1,83 @@
/**
* Translates to a readable values
* @param {Object} instance - The model or context instance
* @param {Object} changes - Object containing changes
*/
exports.translateValues = async(instance, changes) => {
const models = instance.app.models;
function getRelation(instance, property) {
const relations = instance.definition.settings.relations;
for (let relationName in relations) {
const relation = relations[relationName];
if (relation.foreignKey == property)
return relation;
}
return;
}
function getValue(rawData) {
const row = JSON.parse(JSON.stringify(rawData));
for (column in row)
return row[column];
}
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
const properties = Object.assign({}, changes);
for (let property in properties) {
const relation = getRelation(instance, property);
const value = properties[property];
let finalValue = value;
if (relation) {
const model = relation.model;
const row = await models[model].findById(value, {
fields: ['alias', 'name', 'code', 'description']
});
const newValue = getValue(row);
if (newValue) finalValue = newValue;
}
if (finalValue instanceof Date)
finalValue = formatDate(finalValue);
properties[property] = finalValue;
}
return properties;
};
/**
* Returns the changes between two objects
* @param {Object} original - Original object
* @param {Object} changes - New changes
* @return {Object} Old and new values
*/
exports.getChanges = (original, changes) => {
const oldChanges = {};
const newChanges = {};
for (let property in changes) {
if (changes[property] != original[property]) {
newChanges[property] = changes[property];
if (original[property] != undefined)
oldChanges[property] = original[property];
}
}
return {
old: oldChanges,
new: newChanges
};
};

View File

@ -29,7 +29,6 @@ describe('Client updateFiscalData', () => {
const ctx = {req: {accessToken: {userId: 5}}}; const ctx = {req: {accessToken: {userId: 5}}};
ctx.args = {postcode: 46680}; ctx.args = {postcode: 46680};
const client = await app.models.Client.findById(clientId); const client = await app.models.Client.findById(clientId);
expect(client.postcode).toEqual('46460'); expect(client.postcode).toEqual('46460');

View File

@ -43,6 +43,18 @@ module.exports = Self => {
arg: 'provinceFk', arg: 'provinceFk',
type: 'number' type: 'number'
}, },
{
arg: 'sageTaxTypeFk',
type: 'number'
},
{
arg: 'sageTransactionTypeFk',
type: 'number'
},
{
arg: 'transferorFk',
type: 'number'
},
{ {
arg: 'hasToInvoiceByAddress', arg: 'hasToInvoiceByAddress',
type: 'boolean' type: 'boolean'

View File

@ -83,6 +83,12 @@
"SmsConfig": { "SmsConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },
"SageTaxType": {
"dataSource": "vn"
},
"SageTransactionType": {
"dataSource": "vn"
},
"TpvError": { "TpvError": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -118,6 +118,18 @@
}, },
"created": { "created": {
"type": "Date" "type": "Date"
},
"sageTaxTypeFk": {
"type": "number",
"mysql": {
"columnName": "taxTypeSageFk"
}
},
"sageTransactionTypeFk": {
"type": "number",
"mysql": {
"columnName": "transactionTypeSageFk"
}
} }
}, },
"relations": { "relations": {
@ -200,6 +212,20 @@
"type": "hasOne", "type": "hasOne",
"model": "ClaimRatio", "model": "ClaimRatio",
"foreignKey": "clientFk" "foreignKey": "clientFk"
},
"transferor": {
"type": "belongsTo",
"model": "Client",
"foreignKey": "transferorFk"
} }
} },
"scopes": {
"isActive": {
"where": {
"isActive": {
"neq": false
}
}
}
}
} }

View File

@ -0,0 +1,33 @@
{
"name": "SageTaxType",
"base": "VnModel",
"options": {
"mysql": {
"table": "sage.TiposIva"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier",
"mysql": {
"columnName": "CodigoIva"
}
},
"vat": {
"type": "string",
"mysql": {
"columnName": "Iva"
}
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -0,0 +1,33 @@
{
"name": "SageTransactionType",
"base": "VnModel",
"options": {
"mysql": {
"table": "sage.TiposTransacciones"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier",
"mysql": {
"columnName": "CodigoTransaccion"
}
},
"transaction": {
"type": "string",
"mysql": {
"columnName": "Transaccion"
}
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -1,6 +1,6 @@
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="CreditClassifications/{{post.params.classificationId}}/insurances" url="CreditClassifications/{{$ctrl.$params.classificationId}}/insurances"
data="$ctrl.insurance" data="$ctrl.insurance"
insert-mode="true" insert-mode="true"
form="form"> form="form">
@ -34,7 +34,7 @@
<vn-submit label="Save"></vn-submit> <vn-submit label="Save"></vn-submit>
<vn-button <vn-button
label="Cancel" label="Cancel"
ui-sref="client.card.creditInsurance.insurance.index({classificationId: post.params.classificationId})"> ui-sref="client.card.creditInsurance.insurance.index({classificationId: $ctrl.$params.classificationId})">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>

View File

@ -1,4 +1,5 @@
Simple ticket: Ticket simple Simple ticket: Ticket simple
View consumer report: Ver informe de consumo View consumer report: Ver informe de consumo
From date: Fecha desde From date: Fecha desde
To date: Fecha hasta To date: Fecha hasta
Go to user: Ir al usuario

View File

@ -18,6 +18,18 @@
data="countries" data="countries"
order="country"> order="country">
</vn-crud-model> </vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTaxTypes"
data="sageTaxTypes"
order="vat">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTransactionTypes"
data="sageTransactionTypes"
order="transaction">
</vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md"> <form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
@ -44,6 +56,35 @@
rule> rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTaxTypeFk"
data="sageTaxTypes"
show-field="vat"
value-field="id"
label="Sage tax type"
rule>
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTransactionTypeFk"
data="sageTransactionTypes"
show-field="transaction"
value-field="id"
label="Sage transaction type"
rule>
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.client.transferorFk"
url="Clients/isActive"
search-function="$ctrl.transferorSearchFunction($search)"
where="{id: {neq: $ctrl.client.id}}"
show-field="name"
value-field="id"
label="Previous client"
info="In case of a company succession, specify the grantor company"
rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-datalist vn-one <vn-datalist vn-one
label="Postcode" label="Postcode"

View File

@ -163,6 +163,12 @@ export default class Controller extends Section {
this.client.provinceFk = response.provinceFk; this.client.provinceFk = response.provinceFk;
this.client.countryFk = response.countryFk; this.client.countryFk = response.countryFk;
} }
transferorSearchFunction($search) {
return /^\d+$/.test($search)
? {id: $search}
: {name: {like: '%' + $search + '%'}};
}
} }
ngModule.vnComponent('vnClientFiscalData', { ngModule.vnComponent('vnClientFiscalData', {

View File

@ -5,4 +5,8 @@ Frozen: Congelado
In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not.: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automáticamente el cambio a todos los consignatarios, en caso contrario preguntará al usuario si quiere o no propagar. In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not.: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automáticamente el cambio a todos los consignatarios, en caso contrario preguntará al usuario si quiere o no propagar.
You can use letters and spaces: Se pueden utilizar letras y espacios You can use letters and spaces: Se pueden utilizar letras y espacios
Found a client with this data: Se ha encontrado un cliente con estos datos Found a client with this data: Se ha encontrado un cliente con estos datos
Found a client with this phone or email: El cliente con id <a href="#!/client/{{clientId}}/summary" target="_blank">{{clientId}}</a> ya tiene este teléfono o email. <br/> ¿Quieres continuar? Found a client with this phone or email: El cliente con id <a href="#!/client/{{clientId}}/summary" target="_blank">{{clientId}}</a> ya tiene este teléfono o email. <br/> ¿Quieres continuar?
Sage tax type: Tipo de impuesto Sage
Sage transaction type: Tipo de transacción Sage
Previous client: Cliente anterior
In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente

View File

@ -40,8 +40,7 @@
vn-tooltip="Client frozen" vn-tooltip="Client frozen"
icon="icon-frozen"> icon="icon-frozen">
</vn-icon-button> </vn-icon-button>
<vn-icon-button <vn-icon-button vn-anchor="{state: 'ticket.index', params: {q: {clientFk: client.id}}}"
ng-click="$ctrl.filterTickets(client, $event)"
vn-tooltip="Client tickets" vn-tooltip="Client tickets"
icon="icon-ticket"> icon="icon-ticket">
</vn-icon-button> </vn-icon-button>

View File

@ -41,7 +41,7 @@
<vn-autocomplete vn-three <vn-autocomplete vn-three
ng-show="tag.selection.isFree === false" ng-show="tag.selection.isFree === false"
url="{{$ctrl.sourceTables[itemTag.id].url}}" url="{{$ctrl.sourceTables[itemTag.id].url}}"
search-function="{name: {like: '%'+ $search +'%'}}" search-function="{name: {like: $search +'%'}}"
label="Value" label="Value"
ng-model="itemTag.value" ng-model="itemTag.value"
show-field="{{$ctrl.sourceTables[itemTag.id].field}}" show-field="{{$ctrl.sourceTables[itemTag.id].field}}"

View File

@ -1,5 +1,5 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const diff = require('object-diff'); const loggable = require('vn-loopback/util/log');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('componentUpdate', { Self.remoteMethodCtx('componentUpdate', {
@ -103,10 +103,8 @@ module.exports = Self => {
delete updatedTicket.ctx; delete updatedTicket.ctx;
delete updatedTicket.option; delete updatedTicket.option;
// Force unroute // Force to unroute ticket
const hasToBeUnrouted = true; const hasToBeUnrouted = true;
const changedProperties = diff(originalTicket, updatedTicket);
const query = 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; const query = 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const res = await Self.rawSql(query, [ const res = await Self.rawSql(query, [
id, id,
@ -123,14 +121,18 @@ module.exports = Self => {
option option
]); ]);
const changes = loggable.getChanges(originalTicket, updatedTicket);
const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new);
await models.TicketLog.create({ await models.TicketLog.create({
originFk: id, originFk: id,
userFk: userId, userFk: userId,
action: 'update', action: 'update',
changedModel: 'Ticket', changedModel: 'Ticket',
changedModelId: id, changedModelId: id,
oldInstance: originalTicket, oldInstance: oldProperties,
newInstance: changedProperties newInstance: newProperties
}); });
const salesPersonId = originalTicket.client().salesPersonFk; const salesPersonId = originalTicket.client().salesPersonFk;
@ -138,20 +140,11 @@ module.exports = Self => {
const origin = ctx.req.headers.origin; const origin = ctx.req.headers.origin;
let changesMade = ''; let changesMade = '';
for (let change in changedProperties) { for (let change in newProperties) {
let value = changedProperties[change]; let value = newProperties[change];
if (value instanceof Date) { let oldValue = oldProperties[change];
value = new Intl.DateTimeFormat('es', {
year: '2-digit',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(value);
}
changesMade += `${change}: ${value}\r\n`; changesMade += `\r\n~${$t(change)}: ${oldValue}~ ➔ *${$t(change)}: ${value}*`;
} }
const message = $t('Changed this data from the ticket', { const message = $t('Changed this data from the ticket', {

View File

@ -50,8 +50,8 @@ describe('ticket componentUpdate()', () => {
req: { req: {
accessToken: {userId: userID}, accessToken: {userId: userID},
headers: {origin: 'http://localhost'}, headers: {origin: 'http://localhost'},
__: (value, params) => { __: value => {
return params.nickname; return value;
} }
} }
}; };

View File

@ -119,7 +119,7 @@
</vn-td> </vn-td>
<vn-td actions> <vn-td actions>
<vn-icon-button <vn-icon-button
vn-click-stop="$ctrl.goToLines(ticket.id)" vn-anchor="{state: 'ticket.card.sale', params: {id: ticket.id}}"
vn-tooltip="Go to lines" vn-tooltip="Go to lines"
icon="icon-lines"> icon="icon-lines">
</vn-icon-button> </vn-icon-button>

View File

@ -114,11 +114,6 @@ export default class Controller extends Section {
return 'warning'; return 'warning';
} }
goToLines(ticketFk) {
let url = this.$state.href('ticket.card.sale', {id: ticketFk}, {absolute: true});
window.open(url, '_blank');
}
preview(ticket) { preview(ticket) {
this.selectedTicket = ticket; this.selectedTicket = ticket;
this.$.summary.show(); this.$.summary.show();

View File

@ -1,7 +1,6 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
// 2066 describe('Worker absences()', () => {
xdescribe('Worker absences()', () => {
it('should get the absence calendar for a full year contract', async() => { it('should get the absence calendar for a full year contract', async() => {
let ctx = {req: {accessToken: {userId: 106}}}; let ctx = {req: {accessToken: {userId: 106}}};
let workerFk = 106; let workerFk = 106;
@ -84,14 +83,16 @@ xdescribe('Worker absences()', () => {
yearStart.setDate(1); yearStart.setDate(1);
const yearEnd = new Date(); const yearEnd = new Date();
yearEnd.setHours(23, 59, 59, 59); const currentYear = yearEnd.getFullYear();
yearEnd.setMonth(11); yearEnd.setFullYear(currentYear + 1);
yearEnd.setDate(31); yearEnd.setHours(0, 0, 0, 0);
yearEnd.setMonth(0);
yearEnd.setDate(1);
const startedTime = yearStart.getTime(); const startedTime = yearStart.getTime();
const endedTime = yearEnd.getTime(); const endedTime = yearEnd.getTime();
const dayTimestamp = 1000 * 60 * 60 * 24; const dayTimestamp = 1000 * 60 * 60 * 24;
const daysInYear = Math.floor((endedTime - startedTime) / dayTimestamp); const daysInYear = Math.round((endedTime - startedTime) / dayTimestamp);
// sets the holidays per year to the amount of days in the current year // sets the holidays per year to the amount of days in the current year
let holidaysConfig = await app.models.WorkCenterHoliday.findOne({ let holidaysConfig = await app.models.WorkCenterHoliday.findOne({
@ -102,17 +103,11 @@ xdescribe('Worker absences()', () => {
let originalHolidaysValue = holidaysConfig.days; let originalHolidaysValue = holidaysConfig.days;
await app.models.WorkCenterHoliday.updateAll( await holidaysConfig.updateAttribute('days', daysInYear);
{
workCenterFk: 1,
year: today.getFullYear()
},
{
days: daysInYear
}
);
// normal test begins // normal test begins
const contract = await app.models.WorkerLabour.findById(106); const userId = 106;
const contract = await app.models.WorkerLabour.findById(userId);
const contractStartDate = contract.started; const contractStartDate = contract.started;
const startingContract = new Date(); const startingContract = new Date();
@ -121,14 +116,13 @@ xdescribe('Worker absences()', () => {
startingContract.setDate(1); startingContract.setDate(1);
await app.models.WorkerLabour.rawSql( await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_start = ? WHERE business_id = ?`, `UPDATE postgresql.business SET date_start = ?, date_end = ? WHERE business_id = ?`,
[startingContract, contract.businessFk] [startingContract, yearEnd, contract.businessFk]
); );
let ctx = {req: {accessToken: {userId: 106}}}; let ctx = {req: {accessToken: {userId: userId}}};
let workerFk = 106;
let result = await app.models.Calendar.absences(ctx, workerFk, yearStart, yearEnd); let result = await app.models.Calendar.absences(ctx, userId, yearStart, yearEnd);
let calendar = result[0]; let calendar = result[0];
let absences = result[1]; let absences = result[1];

View File

@ -29,7 +29,7 @@
<vn-item-section side> <vn-item-section side>
<vn-icon-button <vn-icon-button
ng-click="$ctrl.goToTimeControl($event, worker.id)" ng-click="$ctrl.goToTimeControl($event, worker.id)"
vn-tooltip="Preview" vn-tooltip="Time control"
icon="access_time"> icon="access_time">
</vn-icon-button> </vn-icon-button>
<vn-icon-button <vn-icon-button

View File

@ -53,7 +53,7 @@
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"del": "^2.2.2", "del": "^2.2.2",
"eslint": "^5.14.0", "eslint": "^7.11.0",
"eslint-config-google": "^0.11.0", "eslint-config-google": "^0.11.0",
"eslint-plugin-jasmine": "^2.10.1", "eslint-plugin-jasmine": "^2.10.1",
"fancy-log": "^1.3.2", "fancy-log": "^1.3.2",