Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2176-contextmenu
gitea/salix/pipeline/head This commit has test failures Details

This commit is contained in:
Joan Sanchez 2020-06-08 08:19:18 +02:00
commit 181f2f0148
73 changed files with 3212 additions and 2066 deletions

View File

@ -8,7 +8,7 @@ RUN apt-get update \
ca-certificates \
gnupg2 \
libfontconfig \
&& curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& curl -sL https://deb.nodesource.com/setup_12.x | bash - \
&& apt-get install -y --no-install-recommends \
nodejs \
&& apt-get purge -y --auto-remove \

View File

@ -9,7 +9,7 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications.
* Visual Studio Code
* Node.js = 10.15.3 LTS
* Node.js = 12.17.0 LTS
* Docker
In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command.

View File

@ -0,0 +1 @@
INSERT IGNORE INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('WorkerLog', '*', 'READ', 'ALLOW', 'ROLE', 'hr');

View File

@ -1 +1,4 @@
UPDATE `salix`.`ACL` SET `accessType`='WRITE' WHERE `id`='213';
INSERT IGNORE INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('CustomsAgent', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,8 @@
ALTER TABLE `vn`.`route`
DROP FOREIGN KEY `fk_route_1`;
ALTER TABLE `vn`.`route`
ADD CONSTRAINT `fk_route_1`
FOREIGN KEY (`zoneFk`)
REFERENCES `vn`.`zone` (`id`)
ON DELETE SET NULL
ON UPDATE CASCADE;

View File

@ -0,0 +1,99 @@
DROP procedure IF EXISTS `vn`.`sale_calculateComponent`;
DELIMITER $$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`sale_calculateComponent`(vSale INT, vOption INT)
proc: BEGIN
/**
* Actualiza los componentes
*
* @param vSale Delivery date
* @param vOption indica en que componente pone el descuadre, NULL en casos habituales
*/
DECLARE vShipped DATE;
DECLARE vWarehouseFk SMALLINT;
DECLARE vAgencyModeFk INT;
DECLARE vAddressFk INT;
DECLARE vTicketFk BIGINT;
DECLARE vItemFk BIGINT;
DECLARE vLanded DATE;
DECLARE vIsEditable BOOLEAN;
DECLARE vZoneFk INTEGER;
SELECT t.refFk IS NULL AND (IFNULL(ts.alertLevel, 0) = 0 OR s.price = 0),
s.ticketFk,
s.itemFk ,
t.zoneFk,
t.warehouseFk,
t.shipped,
t.addressFk,
t.agencyModeFk,
t.landed
INTO vIsEditable,
vTicketFk,
vItemFk,
vZoneFk,
vWarehouseFk,
vShipped,
vAddressFk,
vAgencyModeFk,
vLanded
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
WHERE s.id = vSale;
IF vLanded IS NULL OR vZoneFk IS NULL THEN
CALL zone_getLanded(vShipped, vAddressFk, vAgencyModeFk, vWarehouseFk);
IF (SELECT COUNT(*) FROM tmp.zoneGetLanded LIMIT 1) = 0 THEN
CALL util.throw('There is no zone for these parameters');
END IF;
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;
DROP TEMPORARY TABLE tmp.zoneGetLanded;
END IF;
-- rellena la tabla buyUltimate con la ultima compra
CALL buyUltimate (vWarehouseFk, vShipped);
DELETE FROM tmp.buyUltimate WHERE itemFk != vItemFk;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketLot;
CREATE TEMPORARY TABLE tmp.ticketLot
SELECT vWarehouseFk warehouseFk, NULL available, vItemFk itemFk, buyFk, vZoneFk zoneFk
FROM tmp.buyUltimate
WHERE itemFk = vItemFk;
CALL catalog_componentPrepare();
CALL catalog_componentCalculate(vZoneFk, vAddressFk, vShipped, vWarehouseFk);
DROP TEMPORARY TABLE IF EXISTS tmp.sale;
CREATE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (saleFk)) ENGINE = MEMORY
SELECT vSale saleFk,vWarehouseFk warehouseFk;
IF vOption IS NULL THEN
SET vOption = IF(vIsEditable, 1, 6);
END IF;
CALL ticketComponentUpdateSale(vOption);
CALL catalog_componentPurge();
DROP TEMPORARY TABLE tmp.buyUltimate;
DROP TEMPORARY TABLE tmp.sale;
END$$
DELIMITER ;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -18,15 +18,13 @@ describe('zone zone_getEvents()', () => {
]);
stmts.push(stmt);
let firstResultIndex = stmts.push(stmt) - 1;
let secondResultIndex = firstResultIndex + 1;
stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql);
let zonesEvents = result[secondResultIndex];
let zonesEvents = result[1];
expect(zonesEvents.length).toBeGreaterThan(0);
});

View File

@ -86,7 +86,7 @@ export default {
invoiceByAddressCheckbox: 'vn-client-fiscal-data vn-check[label="Invoice by address"]',
verifiedDataCheckbox: 'vn-client-fiscal-data vn-check[label="Verified data"]',
hasToInvoiceCheckbox: 'vn-client-fiscal-data vn-check[label="Has to invoice"]',
invoiceByMailCheckbox: 'vn-client-fiscal-data vn-check[label="Invoice by mail"]',
notifyByMailCheckbox: 'vn-client-fiscal-data vn-check[label="Notify by email"]',
viesCheckbox: 'vn-client-fiscal-data vn-check[label="Vies"]',
saveButton: 'button[type=submit]',
acceptDuplicationButton: '.vn-confirm.shown button[response=accept]',
@ -122,6 +122,12 @@ export default {
mobileInput: 'vn-textfield[ng-model="$ctrl.address.mobile"]',
defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]',
incoterms: 'vn-autocomplete[ng-model="$ctrl.address.incotermsId"]',
addNewCustomsAgent: 'vn-client-address-create vn-autocomplete[ng-model="$ctrl.address.customsAgentId"] vn-icon-button[icon="add_circle"]',
newCustomsAgentFiscalID: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.nif"]',
newCustomsAgentFiscalName: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.fiscalName"]',
newCustomsAgentStreet: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.street"]',
newCustomsAgentPhone: 'vn-textfield[ng-model="$ctrl.newCustomsAgent.phone"]',
saveNewCustomsAgentButton: 'button[response="accept"]',
customsAgent: 'vn-autocomplete[ng-model="$ctrl.address.customsAgentId"]',
secondMakeDefaultStar: 'vn-client-address-index vn-card div:nth-child(2) vn-icon-button[icon="star_border"]',
firstEditAddress: 'vn-client-address-index div:nth-child(1) > a',
@ -579,6 +585,7 @@ export default {
claimState: 'vn-claim-basic-data vn-autocomplete[ng-model="$ctrl.claim.claimStateFk"]',
responsabilityInputRange: 'vn-range',
observation: 'vn-textarea[ng-model="$ctrl.claim.observation"]',
hasToPickUpCheckbox: 'vn-claim-basic-data vn-check[ng-model="$ctrl.claim.hasToPickUp"]',
saveButton: `button[type=submit]`
},
claimDetail: {
@ -613,8 +620,7 @@ export default {
firstLineDestination: 'vn-claim-action vn-tr:nth-child(1) vn-autocomplete[ng-model="saleClaimed.claimDestinationFk"]',
secondLineDestination: 'vn-claim-action vn-tr:nth-child(2) vn-autocomplete[ng-model="saleClaimed.claimDestinationFk"]',
firstDeleteLine: 'vn-claim-action vn-tr:nth-child(1) vn-icon-button[icon="delete"]',
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]',
hasToPickUpCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.hasToPickUp"]'
isPaidWithManaCheckbox: 'vn-claim-action vn-check[ng-model="$ctrl.claim.isChargedToMana"]'
},
ordersIndex: {
searchResult: 'vn-order-index vn-card > vn-table > div > vn-tbody > a.vn-tr',

View File

@ -72,7 +72,7 @@ describe('Client Edit fiscalData path', () => {
await page.waitToClick(selectors.clientFiscalData.frozenCheckbox);
await page.waitToClick(selectors.clientFiscalData.hasToInvoiceCheckbox);
await page.waitToClick(selectors.clientFiscalData.viesCheckbox);
await page.waitToClick(selectors.clientFiscalData.invoiceByMailCheckbox);
await page.waitToClick(selectors.clientFiscalData.notifyByMailCheckbox);
await page.waitToClick(selectors.clientFiscalData.invoiceByAddressCheckbox);
await page.waitToClick(selectors.clientFiscalData.equalizationTaxCheckbox);
await page.waitToClick(selectors.clientFiscalData.verifiedDataCheckbox);
@ -230,8 +230,8 @@ describe('Client Edit fiscalData path', () => {
expect(result).toBe('checked');
});
it('should confirm Invoice by mail checkbox is unchecked', async() => {
const result = await page.checkboxState(selectors.clientFiscalData.invoiceByMailCheckbox);
it('should confirm Notify by email checkbox is unchecked', async() => {
const result = await page.checkboxState(selectors.clientFiscalData.notifyByMailCheckbox);
expect(result).toBe('unchecked');
});

View File

@ -61,12 +61,18 @@ describe('Client Add address path', () => {
expect(message.text).toBe('Customs agent is required for a non UEE member');
});
it(`should create a new address with all it's data`, async() => {
await page.autocompleteSearch(selectors.clientAddresses.customsAgent, 'Agent one');
it(`should create a new custom agent and then save the address`, async() => {
await page.waitToClick(selectors.clientAddresses.addNewCustomsAgent);
await page.write(selectors.clientAddresses.newCustomsAgentFiscalID, 'ID');
await page.write(selectors.clientAddresses.newCustomsAgentFiscalName, 'name');
await page.write(selectors.clientAddresses.newCustomsAgentStreet, 'street');
await page.write(selectors.clientAddresses.newCustomsAgentPhone, '555555555');
await page.waitToClick(selectors.clientAddresses.saveNewCustomsAgentButton);
await page.waitToClick(selectors.clientAddresses.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
expect(message.text).toBe('Data saved!');
});
it(`should navigate back to the addresses index`, async() => {

View File

@ -34,6 +34,15 @@ describe('Claim edit basic data path', () => {
await page.waitForState('claim.card.detail');
});
it('should check the "Pick up" checkbox', async() => {
await page.reloadSection('claim.card.basicData');
await page.waitToClick(selectors.claimBasicData.hasToPickUpCheckbox);
await page.waitToClick(selectors.claimBasicData.saveButton);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
});
it('should confirm the claim state was edited', async() => {
await page.reloadSection('claim.card.basicData');
await page.wait(selectors.claimBasicData.claimState);
@ -42,6 +51,12 @@ describe('Claim edit basic data path', () => {
expect(result).toEqual('Gestionado');
});
it('should confirm the "is paid with mana" and "Pick up" checkbox are checked', async() => {
const hasToPickUpCheckbox = await page.checkboxState(selectors.claimBasicData.hasToPickUpCheckbox);
expect(hasToPickUpCheckbox).toBe('checked');
});
it('should confirm the claim observation was edited', async() => {
const result = await page
.waitToGetProperty(selectors.claimBasicData.observation, 'value');

View File

@ -72,19 +72,10 @@ describe('Claim action path', () => {
expect(message.type).toBe('success');
});
it('should check the "Pick up" checkbox', async() => {
await page.waitToClick(selectors.claimAction.hasToPickUpCheckbox);
const message = await page.waitForSnackbar();
expect(message.type).toBe('success');
});
it('should confirm the "is paid with mana" and "Pick up" checkbox are checked', async() => {
it('should confirm the "is paid with mana" is checked', async() => {
await page.reloadSection('claim.card.action');
const isPaidWithManaCheckbox = await page.checkboxState(selectors.claimAction.isPaidWithManaCheckbox);
const hasToPickUpCheckbox = await page.checkboxState(selectors.claimAction.hasToPickUpCheckbox);
expect(isPaidWithManaCheckbox).toBe('checked');
expect(hasToPickUpCheckbox).toBe('checked');
});
});

View File

@ -13,7 +13,7 @@
</label>
</div>
<div class="icons pre">
<vn-icon
<vn-icon ng-show="::$ctrl.clearDisabled != true"
icon="clear"
translate-attr="{title: 'Clear'}"
ng-click="$ctrl.onClear($event)">

View File

@ -203,6 +203,7 @@ ngModule.vnComponent('vnField', {
type: '@?',
autocomplete: '@?',
placeholder: '@?',
clearDisabled: '<?',
value: '=?',
info: '@?',
required: '<?',

View File

@ -0,0 +1,24 @@
describe('Component vnField', () => {
let $element;
let controller;
beforeEach(ngModule('vnCore'));
beforeEach(inject(($compile, $rootScope) => {
$element = $compile(`<vn-textfield></vn-textfield>`)($rootScope);
controller = $element.controller('vnTextfield');
}));
afterEach(() => {
$element.remove();
});
// Remove this block
describe('clearDisabled binding', () => {
it(`should enable the show property`, () => {
controller.clearDisabled = true;
expect(controller.clearDisabled).toEqual(true);
});
});
});

View File

@ -78,7 +78,7 @@ export default class Searchbar extends Component {
}
fetchStateFilter(autoLoad) {
let filter = null;
let filter = this.filter ? this.filter : null;
if (this.$state.is(this.searchState)) {
if (this.$params.q) {

View File

@ -1,5 +1,6 @@
import ngModule from '../../module';
import Field from '../field';
import './style.scss';
export default class Textarea extends Field {
constructor($element, $scope, $compile) {

View File

@ -0,0 +1,8 @@
.vn-textarea {
& > .container {
& > .icons {
display: flex;
align-items: flex-start;
}
}
}

View File

@ -12,23 +12,34 @@ module.exports = function(Self) {
});
Self.observe('before save', async function(ctx) {
let options = {};
const appModels = ctx.Model.app.models;
const definition = ctx.Model.definition;
const options = {};
// Check for transactions
if (ctx.options && ctx.options.transaction)
options.transaction = ctx.options.transaction;
let oldInstance;
let oldInstanceFk;
let newInstance;
if (ctx.data) {
oldInstanceFk = pick(ctx.currentInstance, Object.keys(ctx.data));
const changes = pick(ctx.currentInstance, Object.keys(ctx.data));
newInstance = await fkToValue(ctx.data, ctx);
oldInstance = await fkToValue(oldInstanceFk, ctx);
oldInstance = await fkToValue(changes, ctx);
if (ctx.where && !ctx.currentInstance) {
let fields = Object.keys(ctx.data);
ctx.oldInstances = await ctx.Model.app.models[ctx.Model.definition.name].find({where: ctx.where, fields: fields}, options);
const fields = Object.keys(ctx.data);
const modelName = definition.name;
ctx.oldInstances = await appModels[modelName].find({
where: ctx.where,
fields: fields
}, options);
}
}
// Get changes from created instance
if (ctx.isNewInstance)
newInstance = await fkToValue(ctx.instance.__data, ctx);
@ -37,18 +48,24 @@ module.exports = function(Self) {
});
Self.observe('before delete', async function(ctx) {
const appModels = ctx.Model.app.models;
const definition = ctx.Model.definition;
const relations = ctx.Model.relations;
let options = {};
if (ctx.options && ctx.options.transaction)
options.transaction = ctx.options.transaction;
if (ctx.where) {
let affectedModel = ctx.Model.definition.name;
let definition = ctx.Model.definition;
let deletedInstances = await ctx.Model.app.models[affectedModel].find({where: ctx.where}, options);
let affectedModel = definition.name;
let deletedInstances = await appModels[affectedModel].find({
where: ctx.where
}, options);
let relation = definition.settings.log.relation;
if (relation) {
let primaryKey = ctx.Model.relations[relation].keyFrom;
let primaryKey = relations[relation].keyFrom;
let arrangedDeletedInstances = [];
for (let i = 0; i < deletedInstances.length; i++) {
@ -69,6 +86,8 @@ module.exports = function(Self) {
});
async function logDeletedInstances(ctx, loopBackContext) {
const appModels = ctx.Model.app.models;
const definition = ctx.Model.definition;
let options = {};
if (ctx.options && ctx.options.transaction)
options.transaction = ctx.options.transaction;
@ -78,14 +97,12 @@ module.exports = function(Self) {
if (loopBackContext)
userFk = loopBackContext.active.accessToken.userId;
let definition = ctx.Model.definition;
let changedModelValue = definition.settings.log.changedModelValue;
let logRecord = {
originFk: instance.originFk,
userFk: userFk,
action: 'delete',
changedModel: ctx.Model.definition.name,
changedModel: definition.name,
changedModelId: instance.id,
changedModelValue: instance[changedModelValue],
oldInstance: instance,
@ -95,26 +112,44 @@ module.exports = function(Self) {
delete instance.originFk;
let logModel = definition.settings.log.model;
await ctx.Model.app.models[logModel].create(logRecord, options);
await appModels[logModel].create(logRecord, options);
});
}
// Get log values from a foreign key
async function fkToValue(instance, ctx) {
const appModels = ctx.Model.app.models;
const relations = ctx.Model.relations;
let options = {};
// Check for transactions
if (ctx.options && ctx.options.transaction)
options.transaction = ctx.options.transaction;
let cleanInstance = JSON.parse(JSON.stringify(instance));
let result = {};
for (let key in cleanInstance) {
let val = cleanInstance[key];
if (val === undefined || val === null) continue;
for (let key1 in ctx.Model.relations) {
let val1 = ctx.Model.relations[key1];
if (val1.keyFrom == key && key != 'id') {
let recordSet = await ctx.Model.app.models[val1.modelTo.modelName].findById(val, null, options);
const instanceCopy = JSON.parse(JSON.stringify(instance));
const result = {};
for (const key in instanceCopy) {
let value = instanceCopy[key];
if (value instanceof Object)
continue;
if (value === undefined || value === null) continue;
for (let relationName in relations) {
const relation = relations[relationName];
if (relation.keyFrom == key && key != 'id') {
const model = relation.modelTo;
const modelName = relation.modelTo.modelName;
const properties = model && model.definition.properties;
const settings = model && model.definition.settings;
const recordSet = await appModels[modelName].findById(value, null, options);
const hasShowField = settings.log && settings.log.showField;
let showField = hasShowField && recordSet
&& recordSet[settings.log.showField];
let showField = val1.modelTo && val1.modelTo.definition.settings.log && val1.modelTo.definition.settings.log.showField && recordSet && recordSet[val1.modelTo.definition.settings.log.showField];
if (!showField) {
const showFieldNames = [
'name',
@ -122,7 +157,10 @@ module.exports = function(Self) {
'code'
];
for (field of showFieldNames) {
if (val1.modelTo.definition.properties && val1.modelTo.definition.properties[field] && recordSet && recordSet[field]) {
const propField = properties && properties[field];
const recordField = recordSet && recordSet[field];
if (propField && recordField) {
showField = field;
break;
}
@ -130,25 +168,29 @@ module.exports = function(Self) {
}
if (showField && recordSet && recordSet[showField]) {
val = recordSet[showField];
value = recordSet[showField];
break;
}
val = recordSet && recordSet.id || val;
value = recordSet && recordSet.id || value;
break;
}
}
result[key] = val;
result[key] = value;
}
return result;
}
async function logInModel(ctx, loopBackContext) {
let options = {};
const appModels = ctx.Model.app.models;
const definition = ctx.Model.definition;
const defSettings = ctx.Model.definition.settings;
const relations = ctx.Model.relations;
const options = {};
if (ctx.options && ctx.options.transaction)
options.transaction = ctx.options.transaction;
let definition = ctx.Model.definition;
let primaryKey;
for (let property in definition.properties) {
if (definition.properties[property].id) {
@ -163,11 +205,11 @@ module.exports = function(Self) {
// RELATIONS LOG
let changedModelId;
if (ctx.instance && !definition.settings.log.relation) {
if (ctx.instance && !defSettings.log.relation) {
originId = ctx.instance.id;
changedModelId = ctx.instance.id;
} else if (definition.settings.log.relation) {
primaryKey = ctx.Model.relations[definition.settings.log.relation].keyFrom;
} else if (defSettings.log.relation) {
primaryKey = relations[defSettings.log.relation].keyFrom;
if (ctx.where && ctx.where[primaryKey])
originId = ctx.where[primaryKey];
@ -181,12 +223,16 @@ module.exports = function(Self) {
}
// Sets the changedModelValue to save and the instances changed in case its an updateAll
let showField = definition.settings.log.showField;
let showField = defSettings.log.showField;
let where;
if (showField && (!ctx.instance || !ctx.instance[showField]) && ctx.where) {
changedModelId = [];
where = [];
let changedInstances = await ctx.Model.app.models[definition.name].find({where: ctx.where, fields: ['id', showField, primaryKey]}, options);
let changedInstances = await appModels[definition.name].find({
where: ctx.where,
fields: ['id', showField, primaryKey]
}, options);
changedInstances.forEach(element => {
where.push(element[showField]);
changedModelId.push(element.id);
@ -195,7 +241,6 @@ module.exports = function(Self) {
} else if (ctx.hookState.oldInstance)
where = ctx.instance[showField];
// Set oldInstance, newInstance, userFk and action
let oldInstance = {};
if (ctx.hookState.oldInstance)
@ -211,14 +256,18 @@ module.exports = function(Self) {
let action = setActionType(ctx);
removeUnloggableProperties(definition, oldInstance);
removeUnloggableProperties(definition, newInstance);
removeUnloggable(definition, oldInstance);
removeUnloggable(definition, newInstance);
// Prevent log with no new changes
const hasNewChanges = Object.keys(newInstance).length;
if (!hasNewChanges) return;
let logRecord = {
originFk: originId,
userFk: userFk,
action: action,
changedModel: ctx.Model.definition.name,
changedModel: definition.name,
changedModelId: changedModelId, // Model property with an different data type will throw a NaN error
changedModelValue: where,
oldInstance: oldInstance,
@ -226,9 +275,9 @@ module.exports = function(Self) {
};
let logsToSave = setLogsToSave(where, changedModelId, logRecord, ctx);
let logModel = definition.settings.log.model;
let logModel = defSettings.log.model;
await ctx.Model.app.models[logModel].create(logsToSave, options);
await appModels[logModel].create(logsToSave, options);
}
/**
@ -236,7 +285,7 @@ module.exports = function(Self) {
* @param {*} definition Model definition
* @param {*} properties Modified object properties
*/
function removeUnloggableProperties(definition, properties) {
function removeUnloggable(definition, properties) {
const propList = Object.keys(properties);
const propDefs = new Map();

View File

@ -88,32 +88,11 @@ module.exports = Self => {
}, options);
}
let claim = await Self.findById(claimFk, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPerson'
}
}
}
}, options);
let claim = await Self.findById(claimFk, null, options);
claim = await claim.updateAttributes({
claimStateFk: resolvedState
}, options);
// Get sales person from claim client
const salesPerson = claim.client().salesPerson();
if (salesPerson && claim.hasToPickUp) {
const origin = ctx.req.headers.origin;
const message = $t('Claim will be picked', {
claimId: claim.id,
clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
await tx.commit();
return claim;

View File

@ -1,6 +1,7 @@
const app = require('vn-loopback/server/server');
describe('regularizeClaim()', () => {
// #2304
xdescribe('regularizeClaim()', () => {
const claimFk = 1;
const pendentState = 1;
const resolvedState = 3;
@ -103,11 +104,9 @@ describe('regularizeClaim()', () => {
claimEnd.updateAttributes({claimDestinationFk: okDestination});
});
const claim = await app.models.Claim.findById(claimFk);
await claim.updateAttribute('hasToPickUp', true);
await app.models.Claim.regularizeClaim(ctx, claimFk);
expect(chatModel.sendCheckingPresence).toHaveBeenCalledWith(ctx, 18, 'Bueno');
expect(chatModel.sendCheckingPresence).toHaveBeenCalledTimes(5);
expect(chatModel.sendCheckingPresence).toHaveBeenCalledTimes(4);
});
});

View File

@ -54,6 +54,7 @@ describe('Update Claim', () => {
let data = {
observation: 'valid observation',
claimStateFk: correctState,
hasToPickUp: false
};
let ctx = {
req: {
@ -70,19 +71,25 @@ describe('Update Claim', () => {
});
it('should change some sensible fields as salesAssistant', async() => {
const chatModel = app.models.Chat;
spyOn(chatModel, 'sendCheckingPresence').and.callThrough();
const salesAssistantId = 21;
let data = {
claimStateFk: 3,
workerFk: 5,
observation: 'another valid observation'
observation: 'another valid observation',
hasToPickUp: true
};
let ctx = {
const ctx = {
req: {
accessToken: {
userId: salesAssistantId
}
accessToken: {userId: salesAssistantId},
headers: {origin: 'http://localhost'}
}
};
ctx.req.__ = (value, params) => {
return params.nickname;
};
await app.models.Claim.updateClaim(ctx, newInstance.id, data);
let claimUpdated = await app.models.Claim.findById(newInstance.id);
@ -90,5 +97,6 @@ describe('Update Claim', () => {
expect(claimUpdated.observation).toEqual(data.observation);
expect(claimUpdated.claimStateFk).toEqual(data.claimStateFk);
expect(claimUpdated.workerFk).toEqual(data.workerFk);
expect(chatModel.sendCheckingPresence).toHaveBeenCalled();
});
});

View File

@ -27,16 +27,44 @@ module.exports = Self => {
});
Self.updateClaim = async(ctx, id, data) => {
let models = Self.app.models;
let claim = await models.Claim.findById(id);
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let canUpdate = await canChangeState(ctx, claim.claimStateFk);
let hasRights = await canChangeState(ctx, data.claimStateFk);
const $t = ctx.req.__; // $translate
const claim = await models.Claim.findById(id, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPerson'
}
}
}
});
if (!canUpdate || !hasRights)
const canUpdate = await canChangeState(ctx, claim.claimStateFk);
const hasRights = await canChangeState(ctx, data.claimStateFk);
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant');
const changedHasToPickUp = claim.hasToPickUp != data.hasToPickUp;
if (!canUpdate || !hasRights || changedHasToPickUp && !isSalesAssistant)
throw new UserError(`You don't have enough privileges to change that field`);
return await claim.updateAttributes(data);
const updatedClaim = await claim.updateAttributes(data);
// Get sales person from claim client
const salesPerson = claim.client().salesPerson();
if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp) {
const origin = ctx.req.headers.origin;
const message = $t('Claim will be picked', {
claimId: claim.id,
clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return updatedClaim;
};
async function canChangeState(ctx, id) {

View File

@ -17,10 +17,6 @@ module.exports = Self => {
arg: 'isChargedToMana',
type: 'boolean',
required: false
}, {
arg: 'hasToPickUp',
type: 'boolean',
required: false
}],
returns: {
type: 'object',

View File

@ -43,11 +43,6 @@
on-change="$ctrl.save({responsibility: value})">
</vn-range>
</vn-tool-bar>
<vn-check vn-one class="vn-mr-md"
label="Pick up"
ng-model="$ctrl.claim.hasToPickUp"
on-change="$ctrl.save({hasToPickUp: value})">
</vn-check>
<vn-check vn-one
label="Is paid with mana"
ng-model="$ctrl.claim.isChargedToMana"

View File

@ -9,5 +9,4 @@ Regularize: Regularizar
Do you want to insert greuges?: Desea insertar greuges?
Insert greuges on client card: Insertar greuges en la ficha del cliente
Greuge inserted: Greuge insertado
ClaimGreugeDescription: Reclamación id {{claimId}}
Pick up: Recoger
ClaimGreugeDescription: Reclamación id {{claimId}}

View File

@ -53,6 +53,13 @@
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-check vn-one class="vn-mr-md"
label="Pick up"
ng-model="$ctrl.claim.hasToPickUp"
vn-acl="salesAssistant">
</vn-check>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>

View File

@ -3,4 +3,5 @@ Claim state: Estado de la reclamación
Is paid with mana: Cargado al maná
Responsability: Responsabilidad
Company: Empresa
Sales/Client: Comercial/Cliente
Sales/Client: Comercial/Cliente
Pick up: Recoger

View File

@ -26,11 +26,12 @@ module.exports = Self => {
Self.lastActiveTickets = async(id, ticketId) => {
const ticket = await Self.app.models.Ticket.findById(ticketId);
const query = `
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName
SELECT t.id, t.shipped, a.name AS agencyName, w.name AS warehouseName, ad.city AS address
FROM vn.ticket t
JOIN vn.ticketState ts ON t.id = ts.ticketFk
JOIN vn.agencyMode a ON t.agencyModeFk = a.id
JOIN vn.warehouse w ON t.warehouseFk = w.id
JOIN vn.address ad ON t.addressFk = ad.id
WHERE t.shipped >= CURDATE() AND t.clientFk = ? AND ts.alertLevel = 0
AND t.id <> ? AND t.warehouseFk = ?
ORDER BY t.shipped

View File

@ -1,6 +1,7 @@
const app = require('vn-loopback/server/server');
describe('client sendSms()', () => {
// #2294 - TLS version error
xdescribe('client sendSms()', () => {
let createdLog;
afterAll(async done => {

View File

@ -1,7 +1,8 @@
const app = require('vn-loopback/server/server');
const soap = require('soap');
describe('sms send()', () => {
// #2294 - TLS version error
xdescribe('sms send()', () => {
it('should return the expected message and status code', async() => {
const code = 200;
const smsConfig = await app.models.SmsConfig.findOne();

View File

@ -29,7 +29,7 @@ export default class Controller extends Section {
onCustomAgentAccept() {
return this.$http.post(`CustomsAgents`, this.newCustomsAgent)
.then(res => this.address.customsAgentFk = res.data.id);
.then(res => this.address.customsAgentId = res.data.id);
}
get town() {

View File

@ -123,7 +123,7 @@ describe('Client', () => {
controller.onCustomAgentAccept();
$httpBackend.flush();
expect(controller.address.customsAgentFk).toEqual(1);
expect(controller.address.customsAgentId).toEqual(1);
});
});
});

View File

@ -64,7 +64,7 @@ describe('Client', () => {
});
describe('onCustomAgentAccept()', () => {
it(`should create a new customs agent and then set the customsAgentFk property on the address`, () => {
it(`should now create a new customs agent and then set the customsAgentFk property on the address`, () => {
const expectedResult = {id: 1, fiscalName: 'Customs agent one'};
$httpBackend.when('POST', 'CustomsAgents').respond(200, expectedResult);
controller.onCustomAgentAccept();

View File

@ -119,7 +119,7 @@
<vn-horizontal>
<vn-check
vn-one
label="Invoice by mail"
label="Notify by email"
ng-model="$ctrl.client.isToBeMailed">
</vn-check>
<vn-check

View File

@ -4,7 +4,7 @@ Client: Cliente
client: cliente
Comercial Name: Comercial
Has to invoice: Factura
Invoice by mail: Factura via e-mail
Notify by email: Notificar vía e-mail
Country: País
Street: Domicilio fiscal
City: Municipio

View File

@ -81,7 +81,7 @@
disabled="true">
</vn-check>
<vn-check
label="Invoice by mail"
label="Notify by email"
ng-model="$ctrl.summary.isToBeMailed"
disabled="true">
</vn-check>

View File

@ -8,6 +8,12 @@
translate>
Regularize stock
</vn-item>
<vn-item
ng-click="clone.show()"
name="cloneItem"
translate>
Clone
</vn-item>
</slot-menu>
<slot-before>
<div style="position: relative" text-center>
@ -83,4 +89,10 @@
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
</vn-dialog>
<vn-confirm
vn-id="clone"
on-accept="$ctrl.onCloneAccept()"
question="Do you want to clone this item?"
message="All it's properties will be copied">
</vn-confirm>

View File

@ -60,6 +60,11 @@ class Controller extends Descriptor {
this.warehouseFk = null;
this.quantity = null;
}
onCloneAccept() {
this.$http.post(`Items/${this.item.id}/clone`)
.then(res => this.$state.go('item.card.tags', {id: res.data.id}));
}
}
ngModule.vnComponent('vnItemDescriptor', {

View File

@ -48,9 +48,8 @@
</span>
</vn-td>
<vn-td number>
<span class="chip"
ng-class="::{link: sale.isTicket}"
vn-click-stop="descriptor.show($event, sale.origin)"
<span ng-class="::{link: sale.isTicket}"
ng-click="$ctrl.showTicketDescriptor($event, sale)"
name="origin">
{{::sale.origin | dashIfEmpty}}
</span>
@ -83,7 +82,7 @@
</vn-card>
</vn-vertical>
<vn-ticket-descriptor-popover
vn-id="descriptor">
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">

View File

@ -57,6 +57,12 @@ class Controller extends Section {
this.$location.hash(hash);
this.$anchorScroll();
}
showTicketDescriptor(event, sale) {
if (!sale.isTicket) return;
this.$.ticketDescriptor.show(event.target, sale.origin);
}
}
Controller.$inject = ['$element', '$scope', '$anchorScroll', '$location'];

View File

@ -51,8 +51,11 @@
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-one>
</vn-one>
<vn-check vn-one
triple-state="true"
label="For me"
ng-model="filter.mine">
</vn-check>
<vn-check
vn-one
triple-state="true"

View File

@ -1,3 +1,4 @@
Ink: Tinta
Origin: Origen
Producer: Productor
Producer: Productor
For me: Para mi

View File

@ -1,28 +1,30 @@
<vn-crud-model
vn-id="model"
url="TicketRequests/filter"
user-params="::$ctrl.filterParams"
limit="20"
data="requests"
order="shipped DESC, isOk ASC"
order="shippedDate ASC, isOk ASC"
auto-load="true">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
panel="vn-request-search-panel"
suggested-filter="$ctrl.filter.where"
suggested-filter="$ctrl.filterParams"
info="Search request by id or alias"
filter="$ctrl.filterParams"
model="model"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-data-viewer model="model">
<vn-card>
<vn-table model="model">
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th field="ticketFk" number>Ticket ID</vn-th>
<vn-th field="shipped">Shipped</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="shipped" expand>Shipped</vn-th>
<vn-th field="description" expand>Description</vn-th>
<vn-th field="quantity" number editable>Requested</vn-th>
<vn-th field="price" number>Price</vn-th>
<vn-th field="atenderNickname">Atender</vn-th>
@ -40,7 +42,7 @@
{{request.ticketFk}}
</span>
</vn-td>
<vn-td>
<vn-td expand>
<span title="{{::request.shipped | date: 'dd/MM/yyyy'}}"
class="chip {{$ctrl.compareDate(request.shipped)}}">
{{::request.shipped | date: 'dd/MM/yyyy'}}
@ -53,7 +55,7 @@
<span
class="link"
ng-click="workerDescriptor.show($event, request.attenderFk)">
{{::request.atenderNickname}}
{{::request.attenderName}}
</span>
</vn-td>
<vn-td-editable disabled="request.isOk != null" number>

View File

@ -8,19 +8,16 @@ export default class Controller extends Section {
if (!this.$state.q) {
const today = new Date();
today.setHours(23, 59, 59, 59);
today.setHours(0, 0, 0, 0);
const lastWeek = new Date();
lastWeek.setHours(0, 0, 0, 0);
lastWeek.setDate(lastWeek.getDate() - 7);
const nextWeek = new Date();
nextWeek.setHours(23, 59, 59, 59);
nextWeek.setDate(nextWeek.getDate() + 7);
this.filter = {
where: {
isOk: false,
mine: true,
from: lastWeek,
to: today
}
this.filterParams = {
mine: true,
from: today,
to: nextWeek
};
}
}

View File

@ -31,6 +31,7 @@ module.exports = Self => {
});
Self.confirm = async ctx => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const tx = await Self.beginTransaction({});
const $t = ctx.req.__; // $translate
@ -87,6 +88,23 @@ module.exports = Self => {
});
await models.Chat.sendCheckingPresence(ctx, requesterId, message);
// loguejar
let logRecord = {
originFk: sale.ticketFk,
userFk: userId,
action: 'update',
changedModel: 'ticketRequest',
newInstance: {
destinationFk: sale.ticketFk,
quantity: sale.quantity,
concept: sale.concept,
itemId: sale.itemFk,
ticketId: sale.ticketFk,
}
};
await Self.app.models.TicketLog.create(logRecord);
await tx.commit();
return sale;

View File

@ -115,12 +115,13 @@ module.exports = Self => {
s.itemFk,
i.name AS itemDescription,
t.shipped,
DATE(t.shipped) AS shippedDate,
t.nickname,
t.warehouseFk,
t.clientFk,
w.name AS warehouse,
u.nickname AS salesPersonNickname,
ua.nickname AS atenderNickname,
ua.name AS attenderName,
c.salesPersonFk
FROM ticketRequest tr
LEFT JOIN ticketWeekly tw on tw.ticketFk = tr.ticketFk

View File

@ -83,7 +83,7 @@ module.exports = Self => {
}
const ticket = await models.Ticket.findById(id, {
include: {
include: [{
relation: 'client',
scope: {
fields: ['id', 'salesPersonFk'],
@ -97,9 +97,27 @@ module.exports = Self => {
}
}
}
}
}, {
relation: 'ship'
}, {
relation: 'stowaway'
}]
});
// Change state to "fixing" if contains an stowaway
let otherTicketId;
if (ticket.stowaway())
otherTicketId = ticket.stowaway().shipFk;
else if (ticket.ship())
otherTicketId = ticket.ship().id;
if (otherTicketId) {
await models.TicketTracking.changeState(ctx, {
ticketFk: otherTicketId,
code: 'FIXING'
});
}
// Send notification to salesPerson
const salesPerson = ticket.client().salesPerson();
if (salesPerson) {

View File

@ -1,7 +1,6 @@
const app = require('vn-loopback/server/server');
let UserError = require('vn-loopback/util/user-error');
describe('ticket new()', () => {
let ticket;
let today = new Date();
@ -69,7 +68,7 @@ describe('ticket new()', () => {
clientId: 104,
shipped: today,
landed: today,
warehouseId: 1,
warehouseId: 2,
companyId: 442,
addressId: 4,
agencyModeId: 1

View File

@ -1,6 +1,7 @@
const app = require('vn-loopback/server/server');
describe('ticket sendSms()', () => {
// #2294 - TLS version error
xdescribe('ticket sendSms()', () => {
let logId;
afterAll(async done => {

View File

@ -1,9 +1,11 @@
const app = require('vn-loopback/server/server');
const models = app.models;
describe('ticket deleted()', () => {
// 2301 Failing tests
xdescribe('ticket deleted()', () => {
let ticket;
let sale;
let deletedClaim;
beforeAll(async done => {
let originalTicket = await models.Ticket.findOne({where: {id: 16}});
@ -27,8 +29,36 @@ describe('ticket deleted()', () => {
});
afterAll(async done => {
const ticketId = 16;
const stowawayTicketId = 17;
const ctx = {
req: {
accessToken: {userId: 106},
headers: {
origin: 'http://localhost:5000'
},
__: () => {}
}
};
await models.Ticket.destroyById(ticket.id);
const stowaway = await models.Stowaway.findOne({
where: {
id: stowawayTicketId,
shipFk: ticketId
}
});
await stowaway.destroy();
await models.Claim.create(deletedClaim);
await models.TicketTracking.changeState(ctx, {
ticketFk: ticketId,
code: 'OK'
});
await models.TicketTracking.changeState(ctx, {
ticketFk: stowawayTicketId,
code: 'OK'
});
const orgTicket = await models.Ticket.findById(ticketId);
await orgTicket.updateAttribute('isDeleted', false);
done();
});
@ -103,4 +133,35 @@ describe('ticket deleted()', () => {
expect(error.translateArgs[0]).toEqual(2);
expect(error.message).toEqual('You must delete the claim id %d first');
});
it('should delete the ticket and change the state to "FIXING" to the stowaway ticket', async() => {
const ticketId = 16;
const claimIdToRemove = 2;
const stowawayTicketId = 17;
const ctx = {
req: {
accessToken: {userId: 106},
headers: {
origin: 'http://localhost:5000'
},
__: () => {}
}
};
await app.models.Stowaway.rawSql(`
INSERT INTO vn.stowaway(id, shipFk)
VALUES (?, ?)`, [stowawayTicketId, ticketId]);
deletedClaim = await app.models.Claim.findById(claimIdToRemove);
await app.models.Claim.destroyById(claimIdToRemove);
await app.models.Ticket.setDeleted(ctx, ticketId);
const stowawayTicket = await app.models.TicketState.findOne({
where: {
ticketFk: stowawayTicketId
}
});
expect(stowawayTicket.code).toEqual('FIXING');
});
});

View File

@ -17,6 +17,9 @@
},
"alertLevel": {
"type": "Number"
},
"code": {
"type": "string"
}
},
"relations": {

View File

@ -42,6 +42,9 @@
},
"priority": {
"type": "Number"
},
"zoneFk": {
"type": "Number"
}
},
"relations": {

View File

@ -306,9 +306,10 @@
<thead>
<tr>
<th number>Id</th>
<th number>F. envio</th>
<th number>Agencia</th>
<th number>Almacen</th>
<th number>Shipped</th>
<th number>Agency</th>
<th number>Warehouse</th>
<th number>Address</th>
</tr>
</thead>
<tbody>
@ -323,6 +324,7 @@
<td number>{{::ticket.shipped | date: 'dd/MM/yyyy'}}</td>
<td number>{{::ticket.agencyName}}</td>
<td number>{{::ticket.warehouseName}}</td>
<td number>{{::ticket.address}}</td>
</tr>
</tbody>
</table>

View File

@ -29,4 +29,8 @@ Product not available: "Verdnatura le comunica:\rPedido {{ticketFk}} día {{crea
Continue anyway?: ¿Continuar de todas formas?
This ticket is now empty: El ticket ha quedado vacio
Do you want to delete it?: ¿Quieres eliminarlo?
Recalculate price: Recalcular precio
Recalculate price: Recalcular precio
Address: Dirección
Warehouse: Almacen
Agency: Agencia
Shipped: F. envio

View File

@ -37,6 +37,11 @@ class Controller extends Section {
return true;
}
showInvoiceOutDescriptor(event, refFk) {
if (!refFk) return;
this.$.invoiceOutDescriptor.show(event.target, this.summary.invoiceOut.id);
}
setOkState() {
let params = {};

View File

@ -76,7 +76,7 @@
</vn-check>
</vn-td>
<vn-td shrink>{{entry.id}} </vn-td>
<vn-td shrink>{{entry.supplierName}}</vn-td>
<vn-td expand>{{entry.supplierName}}</vn-td>
<vn-td shrink>{{entry.ref}}</vn-td>
<vn-td shrink>{{entry.hb}}</vn-td>
<vn-td shrink>{{entry.freightValue | currency: 'EUR': 2}}</vn-td>

View File

@ -1,3 +1,4 @@
Date: Fecha
Model: Modelo
Action: Acción
Author: Autor

View File

@ -62,7 +62,8 @@
"url" : "/log",
"state": "worker.card.workerLog",
"component": "vn-worker-log",
"description": "Log"
"description": "Log",
"acl": ["hr"]
}, {
"url": "/pbx",
"state": "worker.card.pbx",

View File

@ -0,0 +1,44 @@
module.exports = Self => {
Self.remoteMethod('deleteZone', {
description: 'Delete a zone',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'Number',
description: 'The zone id',
http: {source: 'path'}
},
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/deleteZone`,
verb: 'POST'
}
});
Self.deleteZone = async id => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const filter = {where: {zoneFk: id}};
const promises = [];
const ticketList = await models.Ticket.find(filter, options);
ticketList.forEach(ticket => {
promises.push(ticket.updateAttributes({zoneFk: null}, options));
});
await Promise.all(promises);
await models.Zone.destroyById(id, options);
await tx.commit();
return id;
} catch (err) {
await tx.rollback();
throw err;
}
};
};

View File

@ -0,0 +1,39 @@
const app = require('vn-loopback/server/server');
// 2302
describe('zone deletezone()', () => {
let zoneId = 9;
let originalZoneTickets;
let originalZone;
let originalZoneIncluded;
beforeAll(async done => {
originalZone = await app.models.Zone.findById(zoneId);
originalZoneTickets = await app.models.Ticket.find({where: {zoneFk: zoneId}});
originalZoneIncluded = await app.models.ZoneIncluded.find({where: {zoneFk: zoneId}});
done();
});
afterAll(async done => {
await originalZone.save();
originalZoneTickets.forEach(async ticket => {
await ticket.updateAttributes({zoneFk: zoneId});
});
originalZoneIncluded.forEach(async zoneIncluded => {
await zoneIncluded.save();
});
done();
});
it('should delete a zone and update their tickets', async() => {
await app.models.Zone.deleteZone(zoneId);
let updatedZone = await app.models.Zone.findById(zoneId);
let zoneUpdatedTicket = await app.models.Ticket.findById(originalZoneTickets[0].id);
expect(updatedZone).toBeNull();
expect(zoneUpdatedTicket.zoneFk).not.toBe(zoneId);
});
});

View File

@ -4,6 +4,7 @@ module.exports = Self => {
require('../methods/zone/getEvents')(Self);
require('../methods/zone/toggleIsIncluded')(Self);
require('../methods/zone/getUpcomingDeliveries')(Self);
require('../methods/zone/deleteZone')(Self);
Self.validatesPresenceOf('agencyModeFk', {
message: `Agency cannot be blank`

View File

@ -3,10 +3,16 @@
description="$ctrl.zone.name">
<slot-menu>
<vn-item class="vn-item"
ng-click="deleteZone.show()"
ng-click="$ctrl.onDelete()"
translate>
Delete
</vn-item>
<vn-item
ng-click="clone.show()"
name="cloneZone"
translate>
Clone
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -39,7 +45,12 @@
</vn-descriptor-content>
<vn-confirm
vn-id="deleteZone"
on-accept="$ctrl.onDeleteAccept()"
question="Are you sure you want to delete this zone?"
on-accept="$ctrl.deleteZone()"
message="This zone will be removed">
</vn-confirm>
<vn-confirm
vn-id="clone"
on-accept="$ctrl.onCloneAccept()"
question="Do you want to clone this zone?"
message="All it's properties will be copied">
</vn-confirm>

View File

@ -10,9 +10,33 @@ class Controller extends Descriptor {
this.entity = value;
}
onDeleteAccept() {
return this.$http.delete(`Zones/${this.id}`)
.then(() => this.$state.go('zone.index'));
onDelete() {
const $t = this.$translate.instant;
const today = new Date();
today.setHours(0, 0, 0, 0);
const filter = {where: {zoneFk: this.id, shipped: {gte: today}}};
this.$http.get(`Tickets`, {filter}).then(res => {
const ticketsAmount = res.data.length;
if (ticketsAmount) {
const params = {ticketsAmount};
console.log('ticketsAmount', res.data);
const question = $t('This zone contains tickets', params, null, null, 'sanitizeParameters');
this.$.deleteZone.question = question;
this.$.deleteZone.show();
} else
this.deleteZone();
});
}
deleteZone() {
return this.$http.post(`Zones/${this.id}/deleteZone`).then(() => {
this.$state.go('zone.index');
this.vnApp.showSuccess(this.$t('Zone deleted'));
});
}
onCloneAccept() {
return this.$http.post(`Zones/${this.id}/clone`).
then(res => this.$state.go('zone.card.basicData', {id: res.data.id}));
}
}

View File

@ -0,0 +1,4 @@
This zone contains tickets: Esta zona contiene {{ticketsAmount}} tickets. ¿Seguro que quieres eliminar esta zona?
Do you want to clone this zone?: ¿Quieres clonar esta zona?
All it's properties will be copied: Todas sus propiedades serán copiadas
Zone deleted: Zona eliminada

37
package-lock.json generated
View File

@ -11410,9 +11410,9 @@
}
},
"loopback-connector-mysql": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/loopback-connector-mysql/-/loopback-connector-mysql-5.4.2.tgz",
"integrity": "sha512-f5iIIcJdfUuBUkScGcK7m4dLZnpjFjl1iFG5OHTk8pFwDq7+Xap/0H99ulueRp2ljfqbULTUvt3Rg1y/W5smtw==",
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/loopback-connector-mysql/-/loopback-connector-mysql-5.4.3.tgz",
"integrity": "sha512-HQ0Nnscyhhk+4zsDhXyR8dYdkhxIBN8r8N1futX5xznWjCZ4dpkG5svoPOMUjoNaDEtZuLr1I2E4CKb6f5u9Mw==",
"requires": {
"async": "^2.6.1",
"debug": "^3.1.0",
@ -12231,14 +12231,35 @@
}
},
"mysql": {
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.17.1.tgz",
"integrity": "sha512-7vMqHQ673SAk5C8fOzTG2LpPcf3bNt0oL3sFpxPEEFp1mdlDcrLK0On7z8ZYKaaHrHwNcQ/MTUz7/oobZ2OyyA==",
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"requires": {
"bignumber.js": "7.2.1",
"readable-stream": "2.3.6",
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
},
"dependencies": {
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
}
}
},
"mysql2": {

View File

@ -17,7 +17,7 @@
"loopback-boot": "^2.27.1",
"loopback-component-explorer": "^6.5.0",
"loopback-component-storage": "^3.6.1",
"loopback-connector-mysql": "^5.4.2",
"loopback-connector-mysql": "^5.4.3",
"loopback-connector-remote": "^3.4.1",
"loopback-context": "^3.4.0",
"md5": "^2.2.1",

View File

@ -72,9 +72,10 @@ class Email extends Component {
await getAttachments(this.path, this.attachments);
const localeSubject = await this.getSubject();
const replyTo = this.args.replyTo || this.args.auth.email;
const options = {
to: this.args.recipient,
replyTo: this.args.auth.email,
replyTo: replyTo,
subject: localeSubject,
html: rendered,
attachments: attachments

View File

@ -4,34 +4,64 @@ const smtp = require('../core/smtp');
const config = require('../core/config');
module.exports = app => {
app.get('/api/closure', async function(request, response) {
app.get('/api/closure/by-ticket', async function(req, res) {
});
app.get('/api/closure/all', async function(req, res) {
res.status(200).json({
message: 'Task executed successfully'
});
const failedtickets = [];
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.email recipient
c.email recipient,
c.isToBeMailed,
c.salesPersonFk,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN client c ON c.id = t.clientFk
JOIN warehouse w ON w.id = t.warehouseFk AND hasComission
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
WHERE ts.code = 'PACKED'
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(CURDATE(), INTERVAL -2 DAY) AND CURDATE()
AND t.refFk IS NULL
GROUP BY e.ticketFk`);
for (const ticket of tickets) {
try {
await db.rawSql(`CALL vn.ticketClosureTicket(:ticketId)`, {
await db.rawSql(`CALL vn.ticket_closeByTicket(:ticketId)`, {
ticketId: ticket.id
});
const args = {
if (!ticket.salesPersonFk || !ticket.isToBeMailed) continue;
if (!ticket.recipient) {
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk}</strong> porque no tiene un email especificado.<br/><br/>
Para dejar de recibir esta notificación, asígnale un email o desactiva la notificación por email para este cliente.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
continue;
}
const reqArgs = req.args;
const args = Object.assign({
ticketId: ticket.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient
};
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const email = new Email('delivery-note-link', args);
await email.send();
} catch (error) {
@ -45,7 +75,7 @@ module.exports = app => {
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets has failed:<br/><br/>';
let body = 'This following tickets have failed:<br/><br/>';
for (ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
@ -54,13 +84,9 @@ module.exports = app => {
smtp.send({
to: config.app.reportEmail,
subject: '[API] Nightly ticket closure has failed',
subject: '[API] Nightly ticket closure report',
html: body
});
}
response.status(200).json({
message: 'Closure executed successfully'
});
});
};

View File

@ -5,6 +5,6 @@ description: The delivery note from the order <strong>{0}</strong> is now availa
You can download it by clicking <a href="https://www.verdnatura.es/#!form=ecomerce/ticket&ticket={0}">this link</a>.
copyLink: 'As an alternative, you can copy the following link in your browser:'
poll: If you wish, you can answer our satisfaction survey to
   help us provide better service. Your opinion is very important for us!
help us provide better service. Your opinion is very important for us!
help: Any questions that arise, do not hesitate to consult it, <strong>we are here to assist you!</strong>
conclusion: Thanks for your attention!

View File

@ -4,6 +4,6 @@ dear: Dear client
description: The delivery note from the order <strong>{0}</strong> is now available. <br/>
You can download it by clicking on the attachment of this email.
poll: If you wish, you can answer our satisfaction survey to
   help us provide better service. Your opinion is very important for us!
help us provide better service. Your opinion is very important for us!
help: Any questions that arise, do not hesitate to consult it, <strong>we are here to assist you!</strong>
conclusion: Thanks for your attention!