Merge branch '2003-adress_validate_uee_members' of verdnatura/salix into dev
gitea/salix/dev This commit has test failures Details

This commit is contained in:
Joan Sanchez 2020-01-30 09:51:11 +00:00 committed by Gitea
commit 3164b7dbee
36 changed files with 949 additions and 270 deletions

View File

@ -19,6 +19,9 @@
},
"code": {
"type": "string"
},
"isUeeMember": {
"type": "Boolean"
}
},
"relations": {

View File

@ -0,0 +1 @@
REPLACE INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('CustomsAgent', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,11 @@
CREATE TABLE `vn`.`customsAgent` (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`fiscalName` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`street` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`nif` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`phone` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`email` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `vn`.`customsAgent`
ADD UNIQUE KEY `nif_UNIQUE` (`nif`);

View File

@ -0,0 +1,10 @@
CREATE TABLE `vn`.`incoterms` (
`code` varchar(3) COLLATE utf8_unicode_ci DEFAULT NULL,
`name` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='Internacional Commercial Terms';
ALTER TABLE `vn`.`incoterms`
ADD PRIMARY KEY (`code`);
REPLACE INTO `vn`.`incoterms` (`code`, `name`) VALUES
('FAS', 'Free Alongside Ship');

View File

@ -0,0 +1,26 @@
ALTER TABLE `vn`.`address`
ADD COLUMN `customsAgentFk` INT NULL DEFAULT NULL AFTER `isEqualizated`;
ALTER TABLE `vn`.`address`
ADD COLUMN `incotermsFk` VARCHAR(3) NULL DEFAULT NULL AFTER `customsAgentFk`;
ALTER TABLE `vn`.`address`
ADD INDEX `address_customsAgentFk_idx` (`customsAgentFk` ASC);
ALTER TABLE `vn`.`address`
ADD INDEX `address_incotermsFk_idx` (`incotermsFk` ASC);
ALTER TABLE `vn`.`address`
ADD CONSTRAINT `address_customsAgentFk`
FOREIGN KEY (`customsAgentFk`)
REFERENCES `vn`.`customsAgent` (`id`)
ON DELETE RESTRICT
ON UPDATE CASCADE;
ALTER TABLE `vn`.`address`
ADD CONSTRAINT `address_incotermsFk`
FOREIGN KEY (`incotermsFk`)
REFERENCES `vn`.`incoterms` (`code`)
ON DELETE RESTRICT
ON UPDATE CASCADE;

View File

@ -78,12 +78,13 @@ INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossF
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`)
VALUES
(1, 'España', 0, 'ES', 1, 24),
(1, 'España', 1, 'ES', 1, 24),
(2, 'Italia', 1, 'IT', 1, 27),
(3, 'Alemania', 1, 'DE', 1, 22),
(4, 'Rumania', 1, 'RO', 1, 24),
(5, 'Holanda', 1, 'NL', 1, 18),
(8, 'Portugal', 1, 'PT', 1, 27),
(13,'Ecuador', 0, 'EC', 1, 24),
(19,'Francia', 1, 'FR', 1, 27),
(30,'Canarias', 1, 'IC', 1, 24);
@ -188,8 +189,8 @@ INSERT INTO `vn`.`province`(`id`, `name`, `countryFk`, `warehouseFk`)
(1, 'Province one', 1, NULL),
(2, 'Province two', 1, NULL),
(3, 'Province three', 1, NULL),
(4, 'Province four', 1, NULL),
(5, 'Province five', 1, NULL);
(4, 'Province four', 2, NULL),
(5, 'Province five', 13, NULL);
INSERT INTO `vn`.`town`(`id`, `name`, `provinceFk`)
VALUES
@ -1961,3 +1962,12 @@ INSERT INTO `vn`.`travelThermograph`(`thermographFk`, `created`, `warehouseFk`,
('TZ1905012010', CURDATE(), 1, 1, 'WARM', 'Temperature in range', 5),
('138350-0', DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1, 'WARM', NULL, 5),
('138350-0', CURDATE(), 1, NULL, 'COOL', NULL, NULL);
REPLACE INTO `vn`.`incoterms` (`code`, `name`)
VALUES
('FAS', 'Free Alongside Ship');
REPLACE INTO `vn`.`customsAgent` (`id`, `fiscalName`, `street`, `nif`, `phone`, `email`)
VALUES
(1, 'Agent one', '1007 Mountain Drive, Gotham', 'N1111111111', '111111111', 'agentone@gotham.com'),
(2, 'Agent two', '1007 Mountain Drive, Gotham', 'N2222222222', '222222222', 'agenttwo@gotham.com');

View File

@ -6,6 +6,7 @@ import {url as defaultURL} from './config';
export async function getBrowser() {
const browser = await Puppeteer.launch({
args: [
'--no-sandbox',
`--window-size=${ 1920 },${ 1080 }`
],
defaultViewport: null,

View File

@ -103,11 +103,13 @@ export default {
streetAddressInput: '[ng-model="$ctrl.address.street"]',
postcodeInput: '[ng-model="$ctrl.address.postalCode"]',
cityInput: '[ng-model="$ctrl.address.city"]',
provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.provinceFk"]',
agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeFk"]',
provinceAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.provinceId"]',
agencyAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.agencyModeId"]',
phoneInput: '[ng-model="$ctrl.address.phone"]',
mobileInput: '[ng-model="$ctrl.address.mobile"]',
defaultAddress: 'vn-client-address-index div:nth-child(1) div[name="street"]',
incotermsAutocomplete: 'vn-autocomplete[ng-model="$ctrl.address.incotermsId"]',
customsAgentAutocomplete: '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',
secondEditAddress: 'vn-client-address-index div:nth-child(2) > a',

View File

@ -74,7 +74,7 @@ describe('Client create path', async() => {
await page.waitToClick(selectors.createClientView.createButton);
const result = await page.waitForLastSnackbar();
expect(result).toEqual(`The postcode doesn't exists. Ensure you put the correct format`);
expect(result).toEqual(`The postcode doesn't exist. Please enter a correct one`);
});
it(`should check for autocompleted city, province and country`, async() => {

View File

@ -26,7 +26,7 @@ describe('Client Add address path', () => {
it('should receive an error after clicking save button as consignee, street and town fields are empty', async() => {
await page.waitToClick(selectors.clientAddresses.defaultCheckboxInput);
await page.autocompleteSearch(selectors.clientAddresses.provinceAutocomplete, 'Province one');
await page.autocompleteSearch(selectors.clientAddresses.provinceAutocomplete, 'Province five');
await page.write(selectors.clientAddresses.cityInput, 'Valencia');
await page.write(selectors.clientAddresses.postcodeInput, '46000');
await page.autocompleteSearch(selectors.clientAddresses.agencyAutocomplete, 'Entanglement');
@ -38,12 +38,29 @@ describe('Client Add address path', () => {
expect(result).toEqual('Some fields are invalid');
});
it(`should create a new address with all it's data`, async() => {
it(`should receive an error after clicking save button as consignee, incoterms and customsAgent are empty`, async() => {
await page.write(selectors.clientAddresses.consigneeInput, 'Bruce Bunner');
await page.write(selectors.clientAddresses.streetAddressInput, '320 Park Avenue New York');
await page.waitToClick(selectors.clientAddresses.saveButton);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('Incoterms is required for a non UEE member');
});
it(`should receive an error after clicking save button as consignee, incoterms and customsAgent are empty`, async() => {
await page.autocompleteSearch(selectors.clientAddresses.incotermsAutocomplete, 'Free Alongside Ship');
await page.waitToClick(selectors.clientAddresses.saveButton);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('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.customsAgentAutocomplete, 'Agent one');
await page.waitToClick(selectors.clientAddresses.saveButton);
const result = await page.waitForLastSnackbar();
expect(result).toEqual('Data saved!');
});

View File

@ -54,12 +54,14 @@
"This ticket can not be modified": "This ticket can not be modified",
"You can't delete a confirmed order": "You can't delete a confirmed order",
"Value has an invalid format": "Value has an invalid format",
"The postcode doesn't exists. Ensure you put the correct format": "The postcode doesn't exists. Ensure you put the correct format",
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one",
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket",
"Has deleted the ticket id": "Has deleted the ticket id [#{{id}}]({{{url}}})",
"Swift / BIC can't be empty": "Swift / BIC can't be empty",
"MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} (#{{itemId}}) for the ticket id [#{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} (#{{clientId}})]({{{url}}}) to *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} (#{{clientId}})]({{{url}}})",
"MESSAGE_CLAIM_ITEM_REGULARIZE": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to {{nickname}} coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})"
"MESSAGE_CLAIM_ITEM_REGULARIZE": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to {{nickname}} coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})",
"Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member",
"Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member"
}

View File

@ -106,7 +106,7 @@
"Invalid quantity": "Cantidad invalida",
"This postal code is not valid": "This postal code is not valid",
"is invalid": "is invalid",
"The postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto",
"The postcode doesn't exist. Please enter a correct one": "El código postal no existe. Por favor, introduce uno correcto",
"The department name can't be repeated": "El nombre del departamento no puede repetirse",
"This phone already exists": "Este teléfono ya existe",
"You cannot move a parent to its own sons": "No puedes mover un elemento padre a uno de sus hijos",
@ -119,6 +119,8 @@
"Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín",
"You should mark at least one week day": "Debes marcar al menos un día de la semana",
"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",
"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_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}}})",

View File

@ -1,44 +0,0 @@
module.exports = function(Self) {
Self.remoteMethod('createDefaultAddress', {
description: 'Creates both client and its web account',
accepts: {
arg: 'data',
type: 'object',
http: {source: 'body'}
},
returns: {
root: true,
type: 'Object'
},
http: {
verb: 'post',
path: '/createDefaultAddress'
}
});
Self.createDefaultAddress = async data => {
const Address = Self.app.models.Address;
const Client = Self.app.models.Client;
const tx = await Address.beginTransaction({});
try {
let options = {transaction: tx};
let address = data.address;
let newAddress = await Address.create(address, options);
let client = await Client.findById(address.clientFk, null, options);
if (data.isDefaultAddress) {
await client.updateAttributes({
defaultAddressFk: newAddress.id
}, options);
}
await tx.commit();
return newAddress;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -1,37 +0,0 @@
const app = require('vn-loopback/server/server');
describe('Address createDefaultAddress', () => {
let address;
let client;
afterAll(async done => {
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
done();
});
it('should verify that client defaultAddressFk is untainted', async() => {
client = await app.models.Client.findById(101);
expect(client.defaultAddressFk).toEqual(1);
});
it('should create a new address and set as a client default address', async() => {
let data = {
address: {
clientFk: 101,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
},
isDefaultAddress: true
};
address = await app.models.Address.createDefaultAddress(data);
client = await app.models.Client.findById(101);
expect(client.defaultAddressFk).toEqual(address.id);
});
});

View File

@ -0,0 +1,121 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('createAddress', {
description: 'Creates client address updating default address',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The client id',
http: {source: 'path'}
},
{
arg: 'nickname',
type: 'String',
required: true
},
{
arg: 'city',
type: 'String',
required: true
},
{
arg: 'street',
type: 'String',
required: true
},
{
arg: 'phone',
type: 'String'
},
{
arg: 'mobile',
type: 'String'
},
{
arg: 'postalCode',
type: 'String'
},
{
arg: 'provinceId',
type: 'Number'
},
{
arg: 'agencyModeId',
type: 'Number'
},
{
arg: 'incotermsId',
type: 'String'
},
{
arg: 'customsAgentId',
type: 'Number'
},
{
arg: 'isActive',
type: 'Boolean'
},
{
arg: 'isDefaultAddress',
type: 'Boolean'
}],
returns: {
root: true,
type: 'Object'
},
http: {
verb: 'post',
path: '/:id/createAddress'
}
});
Self.createAddress = async(ctx, clientId) => {
const models = Self.app.models;
const args = ctx.args;
const tx = await models.Address.beginTransaction({});
try {
const options = {transaction: tx};
const province = await models.Province.findById(args.provinceId, {
include: {
relation: 'country'
}
}, options);
const isUeeMember = province.country().isUeeMember;
if (!isUeeMember && !args.incotermsId)
throw new UserError(`Incoterms is required for a non UEE member`);
if (!isUeeMember && !args.customsAgentId)
throw new UserError(`Customs agent is required for a non UEE member`);
const newAddress = await models.Address.create({
clientFk: clientId,
nickname: args.nickname,
incotermsFk: args.incotermsId,
customsAgentFk: args.customsAgentId,
city: args.city,
street: args.street,
phone: args.phone,
postalCode: args.postalCode,
provinceFk: args.provinceId,
agencyModeFk: args.agencyModeId,
isActive: args.isActive
}, options);
const client = await Self.findById(clientId, null, options);
if (args.isDefaultAddress) {
await client.updateAttributes({
defaultAddressFk: newAddress.id
}, options);
}
await tx.commit();
return newAddress;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,87 @@
const app = require('vn-loopback/server/server');
describe('Address createAddress', () => {
const clientId = 101;
const provinceId = 5;
const incotermsId = 'FAS';
const customAgentOneId = 1;
let address;
let client;
afterAll(async done => {
await client.updateAttributes({defaultAddressFk: 1});
await address.destroy();
done();
});
it('should throw a non uee member error if no incoterms is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
customsAgentId: customAgentOneId
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
} catch (e) {
err = e;
}
expect(err).toBeDefined();
expect(err.message).toEqual('Incoterms is required for a non UEE member');
});
it('should throw a non uee member error if no customsAgent is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceId: provinceId,
nickname: expectedResult,
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId
}
};
try {
await app.models.Client.createAddress(ctx, clientId);
} catch (e) {
err = e;
}
expect(err).toBeDefined();
expect(err.message).toEqual('Customs agent is required for a non UEE member');
});
it('should verify that client defaultAddressFk is untainted', async() => {
client = await app.models.Client.findById(clientId);
expect(client.defaultAddressFk).toEqual(1);
});
it('should create a new address and set as a client default address', async() => {
const ctx = {
args: {
provinceId: 1,
nickname: 'My address',
street: 'Wall Street',
city: 'New York',
incotermsId: incotermsId,
customsAgentId: customAgentOneId,
isDefaultAddress: true
}
};
address = await app.models.Client.createAddress(ctx, clientId);
client = await app.models.Client.findById(clientId);
expect(client.defaultAddressFk).toEqual(address.id);
});
});

View File

@ -0,0 +1,100 @@
const app = require('vn-loopback/server/server');
describe('Address updateAddress', () => {
const clientId = 101;
const addressId = 1;
const provinceId = 5;
const incotermsId = 'FAS';
const customAgentOneId = 1;
let oldAddress;
let address;
afterAll(async done => {
await address.updateAttributes({
nickname: oldAddress.nickname,
provinceFk: 1,
customsAgentFk: null,
incotermsFk: null
});
done();
});
it('should throw a non uee member error if no incoterms is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceFk: provinceId,
nickname: expectedResult,
customsAgentFk: customAgentOneId
}
};
try {
await app.models.Client.updateAddress(ctx, clientId, addressId);
} catch (e) {
err = e;
}
expect(err).toBeDefined();
expect(err.message).toEqual('Incoterms is required for a non UEE member');
});
it('should throw a non uee member error if no customsAgent is defined', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceFk: provinceId,
nickname: expectedResult,
incotermsFk: incotermsId
}
};
try {
await app.models.Client.updateAddress(ctx, clientId, addressId);
} catch (e) {
err = e;
}
expect(err).toBeDefined();
expect(err.message).toEqual('Customs agent is required for a non UEE member');
});
it('should update the adress from a non uee member with no error thrown', async() => {
const expectedResult = 'My edited address';
const ctx = {
args: {
provinceFk: provinceId,
nickname: expectedResult,
incotermsFk: incotermsId,
customsAgentFk: customAgentOneId
}
};
oldAddress = await app.models.Address.findById(addressId);
await app.models.Client.updateAddress(ctx, clientId, addressId);
address = await app.models.Address.findById(addressId);
expect(address.nickname).toEqual(expectedResult);
});
it('should update the address', async() => {
const expectedResult = 'My second time edited address';
const ctx = {
args: {
nickname: expectedResult
}
};
oldAddress = await app.models.Address.findById(addressId);
await app.models.Client.updateAddress(ctx, clientId, addressId);
address = await app.models.Address.findById(addressId);
expect(address.nickname).toEqual(expectedResult);
});
});

View File

@ -0,0 +1,119 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethod('updateAddress', {
description: 'Updates a client address updating default address',
accepts: [{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'clientId',
type: 'Number',
description: 'The client id',
http: {source: 'path'}
},
{
arg: 'addressId',
type: 'Number',
description: 'The address id',
http: {source: 'path'}
},
{
arg: 'nickname',
type: 'String'
},
{
arg: 'city',
type: 'String'
},
{
arg: 'street',
type: 'String'
},
{
arg: 'phone',
type: 'String'
},
{
arg: 'mobile',
type: 'String'
},
{
arg: 'postalCode',
type: 'String'
},
{
arg: 'provinceFk',
type: 'Number'
},
{
arg: 'agencyModeFk',
type: 'Number'
},
{
arg: 'incotermsFk',
type: 'String'
},
{
arg: 'customsAgentFk',
type: 'Number'
},
{
arg: 'isActive',
type: 'Boolean'
},
{
arg: 'isEqualizated',
type: 'Boolean'
}],
returns: {
root: true,
type: 'Object'
},
http: {
verb: 'patch',
path: '/:clientId/updateAddress/:addressId'
}
});
Self.updateAddress = async(ctx, clientId, addressId) => {
const models = Self.app.models;
const args = ctx.args;
const tx = await models.Address.beginTransaction({});
try {
const options = {transaction: tx};
const address = await models.Address.findOne({
where: {
id: addressId,
clientFk: clientId
}
});
const provinceId = args.provinceFk || address.provinceFk;
const province = await models.Province.findById(provinceId, {
include: {
relation: 'country'
}
}, options);
const isUeeMember = province.country().isUeeMember;
const incotermsId = args.incotermsFk || address.incotermsFk;
if (!isUeeMember && !incotermsId)
throw new UserError(`Incoterms is required for a non UEE member`);
const customsAgentId = args.customsAgentFk || address.customsAgentFk;
if (!isUeeMember && !customsAgentId)
throw new UserError(`Customs agent is required for a non UEE member`);
delete args.ctx; // Remove unwanted properties
const updatedAddress = await address.updateAttributes(ctx.args, options);
await tx.commit();
return updatedAddress;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -97,5 +97,11 @@
},
"ClientDms": {
"dataSource": "vn"
},
"CustomsAgent": {
"dataSource": "vn"
},
"Incoterms": {
"dataSource": "vn"
}
}

View File

@ -3,9 +3,6 @@ let getFinalState = require('vn-loopback/util/hook').getFinalState;
let isMultiple = require('vn-loopback/util/hook').isMultiple;
module.exports = Self => {
// Methods
require('../methods/address/createDefaultAddress')(Self);
Self.validateAsync('isEqualizated', cannotHaveET, {
message: 'Cannot check Equalization Tax in this NIF/CIF'
});
@ -25,6 +22,22 @@ module.exports = Self => {
done();
}
Self.validateAsync('postalCode', hasValidPostcode, {
message: `The postcode doesn't exist. Please enter a correct one`
});
async function hasValidPostcode(err, done) {
if (!this.postalCode)
return done();
const models = Self.app.models;
const postcode = await models.Postcode.findById(this.postalCode);
if (!postcode) err();
done();
}
Self.beforeRemote('findById', function(ctx, modelInstance, next) {
ctx.args.filter = {
include: [{
@ -42,21 +55,6 @@ module.exports = Self => {
next();
});
Self.validateAsync('postalCode', hasValidPostcode, {
message: `The postcode doesn't exists. Ensure you put the correct format`
});
async function hasValidPostcode(err, done) {
if (!this.postalCode)
return done();
const models = Self.app.models;
const postcode = await models.Postcode.findById(this.postalCode);
if (!postcode) err();
done();
}
// Helpers
Self.observe('before save', async function(ctx) {

View File

@ -73,6 +73,16 @@
"type": "hasMany",
"model": "AddressObservation",
"foreignKey": "addressFk"
},
"incoterms": {
"type": "belongsTo",
"model": "Incoterms",
"foreignKey": "incotermsFk"
},
"customsAgent": {
"type": "belongsTo",
"model": "CustomsAgent",
"foreignKey": "customsAgentFk"
}
}
}

View File

@ -25,6 +25,8 @@ module.exports = Self => {
require('../methods/client/uploadFile')(Self);
require('../methods/client/lastActiveTickets')(Self);
require('../methods/client/sendSms')(Self);
require('../methods/client/createAddress')(Self);
require('../methods/client/updateAddress')(Self);
// Validations
@ -156,7 +158,7 @@ module.exports = Self => {
}
Self.validateAsync('postCode', hasValidPostcode, {
message: `The postcode doesn't exists. Ensure you put the correct format`
message: `The postcode doesn't exist. Please enter a correct one`
});
async function hasValidPostcode(err, done) {

View File

@ -0,0 +1,33 @@
{
"name": "CustomsAgent",
"base": "VnModel",
"options": {
"mysql": {
"table": "customsAgent"
}
},
"properties": {
"id": {
"type": "Number",
"description": "Identifier",
"id": true
},
"fiscalName": {
"type": "String",
"required": true
},
"street": {
"type": "String"
},
"nif": {
"type": "String",
"required": true
},
"phone": {
"type": "String"
},
"email": {
"type": "String"
}
}
}

View File

@ -0,0 +1,27 @@
{
"name": "Incoterms",
"base": "VnModel",
"options": {
"mysql": {
"table": "incoterms"
}
},
"properties": {
"code": {
"type": "String",
"description": "Identifier",
"id": true
},
"name": {
"type": "String"
}
},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -2,9 +2,10 @@ const app = require('vn-loopback/server/server');
describe('loopback model address', () => {
let createdAddressId;
const clientId = 101;
afterAll(async done => {
let client = await app.models.Client.findById(101);
let client = await app.models.Client.findById(clientId);
await app.models.Address.destroyById(createdAddressId);
await client.updateAttribute('isEqualizated', false);
@ -28,14 +29,14 @@ describe('loopback model address', () => {
});
it('should set isEqualizated to true of a given Client to trigger any new address to have it', async() => {
let client = await app.models.Client.findById(101);
let client = await app.models.Client.findById(clientId);
expect(client.isEqualizated).toBeFalsy();
await client.updateAttribute('isEqualizated', true);
let newAddress = await app.models.Address.create({
clientFk: 101,
clientFk: clientId,
agencyModeFk: 5,
city: 'here',
isActive: true,
@ -44,7 +45,9 @@ describe('loopback model address', () => {
phone: '555555555',
postalCode: '46000',
provinceFk: 1,
street: 'Test address'
street: 'Test address',
incotermsFk: 'FAS',
customsAgentFk: 1
});
expect(newAddress.isEqualizated).toBeTruthy();

View File

@ -1,8 +1,9 @@
<vn-watcher
vn-id="watcher"
url="Addresses/createDefaultAddress"
url="Clients/{{$ctrl.$params.id}}/createAddress"
id-field="id"
data="$ctrl.data"
data="$ctrl.address"
params="$ctrl.address"
save="post"
form="form">
</vn-watcher>
@ -19,7 +20,7 @@
<vn-horizontal>
<vn-check
vn-one
label="Default" ng-model="$ctrl.data.isDefaultAddress">
label="Default" ng-model="$ctrl.address.isDefaultAddress">
</vn-check>
</vn-horizontal>
<vn-horizontal>
@ -41,7 +42,7 @@
<vn-autocomplete
vn-one
vn-id="province"
ng-model="$ctrl.address.provinceFk"
ng-model="$ctrl.address.provinceId"
url="Provinces"
show-field="name"
value-field="id"
@ -97,7 +98,7 @@
<vn-horizontal>
<vn-autocomplete
vn-one
ng-model="$ctrl.address.agencyModeFk"
ng-model="$ctrl.address.agencyModeId"
url="AgencyModes/isActive"
show-field="name"
value-field="id"
@ -116,6 +117,29 @@
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.address.incotermsId"
url="Incoterms"
show-field="name"
value-field="code"
label="Incoterms">
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.address.customsAgentId"
url="CustomsAgents"
show-field="fiscalName"
value-field="id"
label="Customs agent">
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New customs agent"
ng-click="$ctrl.showCustomAgent($event)">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Save"></vn-submit>
@ -130,3 +154,38 @@
<vn-client-postcode vn-id="postcode"
on-response="$ctrl.onResponse($response)">
</vn-client-postcode>
<!-- Create custom agent dialog -->
<vn-dialog class="edit"
vn-id="customAgent"
on-accept="$ctrl.onCustomAgentAccept()">
<tpl-body>
<h5 class="vn-py-sm" translate>New customs agent</h5>
<vn-horizontal>
<vn-textfield vn-one vn-focus
label="NIF"
ng-model="$ctrl.newCustomsAgent.nif"
required="true">
</vn-textfield>
<vn-textfield vn-one
label="Fiscal name"
ng-model="$ctrl.newCustomsAgent.fiscalName"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one
label="Street"
ng-model="$ctrl.newCustomsAgent.street">
</vn-textfield>
<vn-textfield vn-one
label="Phone"
ng-model="$ctrl.newCustomsAgent.phone">
</vn-textfield>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Create</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,17 +1,14 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
export default class Controller {
constructor($, $state) {
this.$ = $;
this.$state = $state;
this.data = {
address: {
clientFk: parseInt($state.params.id),
isActive: true
},
export default class Controller extends Component {
constructor($element, $) {
super($element, $);
this.address = {
isActive: true,
isDefaultAddress: false
};
this.address = this.data.address;
}
get postcodeSelection() {
@ -36,15 +33,27 @@ export default class Controller {
onSubmit() {
this.$.watcher.submit().then(res => {
if (res.data && this.data.isDefaultAddress)
if (this.address.isDefaultAddress)
this.client.defaultAddressFk = res.data.id;
this.$state.go('client.card.address.index');
});
}
showCustomAgent(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.$.customAgent.show();
}
onCustomAgentAccept() {
return this.$http.post(`CustomsAgents`, this.newCustomsAgent)
.then(res => this.address.customsAgentFk = res.data.id);
}
}
Controller.$inject = ['$scope', '$state'];
Controller.$inject = ['$element', '$scope'];
ngModule.component('vnClientAddressCreate', {
template: require('./index.html'),

View File

@ -3,17 +3,21 @@ import watcher from 'core/mocks/watcher';
describe('Client', () => {
describe('Component vnClientAddressCreate', () => {
let $scope;
let controller;
let $componentController;
let $httpBackend;
let $element;
let $state;
beforeEach(ngModule('client'));
beforeEach(angular.mock.inject((_$componentController_, _$state_) => {
$componentController = _$componentController_;
beforeEach(angular.mock.inject(($componentController, $rootScope, _$state_, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$state = _$state_;
$state.params.id = '1234';
controller = $componentController('vnClientAddressCreate', {$state});
$element = angular.element('<vn-client-address-create></vn-client-address-create>');
controller = $componentController('vnClientAddressCreate', {$element, $scope});
controller.$.watcher = watcher;
controller.$.watcher.submit = () => {
return {
@ -26,14 +30,13 @@ describe('Client', () => {
}));
it('should define and set address property', () => {
expect(controller.data.address.clientFk).toBe(1234);
expect(controller.data.address.isActive).toBe(true);
expect(controller.address.isActive).toBe(true);
});
describe('onSubmit()', () => {
it('should perform a PATCH and not set value to defaultAddressFk property', () => {
spyOn(controller.$state, 'go');
controller.data.isDefaultAddress = false;
controller.address.isDefaultAddress = false;
controller.onSubmit();
expect(controller.client.defaultAddressFk).toEqual(121);
@ -42,7 +45,7 @@ describe('Client', () => {
it('should perform a PATCH and set a value to defaultAddressFk property', () => {
spyOn(controller.$state, 'go');
controller.data.isDefaultAddress = true;
controller.address.isDefaultAddress = true;
controller.onSubmit();
expect(controller.client.defaultAddressFk).toEqual(124);
@ -73,5 +76,16 @@ describe('Client', () => {
expect(controller.address.provinceFk).toEqual(1);
});
});
describe('onCustomAgentAccept()', () => {
it(`should 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();
$httpBackend.flush();
expect(controller.address.customsAgentFk).toEqual(1);
});
});
});
});

View File

@ -1,9 +0,0 @@
Street address: Dirección postal
Default: Predeterminado
Consignee: Consignatario
Postcode: Código postal
Town/City: Ciudad
Province: Provincia
Agency: Agencia
Phone: Teléfono
Mobile: Móvil

View File

@ -6,16 +6,16 @@
</mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.address"
url="Addresses"
url="Clients/{{$ctrl.$params.id}}/updateAddress"
id-field="id"
data="$ctrl.address"
form="form">
</vn-watcher>
<vn-crud-model
vn-id="model"
url="AddressObservations"
fields="['id', 'addressFk', 'observationTypeFk', 'description']"
link="{addressFk: $ctrl.$stateParams.addressId}"
link="{addressFk: $ctrl.$params.addressId}"
data="observations"
auto-load="true">
</vn-crud-model>
@ -99,8 +99,8 @@
ng-model="$ctrl.address.postalCode"
rule>
</vn-textfield>
<!--
<vn-icon-button
<!-- <vn-icon-button
vn-auto
class="vn-my-md"
icon="add_circle"
@ -111,7 +111,6 @@
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
initial-data="$ctrl.address.agencyMode"
ng-model="$ctrl.address.agencyModeFk"
url="AgencyModes/isActive"
show-field="name"
@ -131,6 +130,29 @@
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.address.incotermsFk"
url="Incoterms"
show-field="name"
value-field="code"
label="Incoterms">
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.address.customsAgentFk"
url="CustomsAgents"
show-field="fiscalName"
value-field="id"
label="Customs agent">
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New customs agent"
ng-click="$ctrl.showCustomAgent($event)">
</vn-icon-button>
</append>
</vn-autocomplete>
</vn-horizontal>
<vn-title>Notes</vn-title>
<div name="observations">
<vn-horizontal ng-repeat="observation in observations">
@ -178,3 +200,37 @@
on-response="$ctrl.onResponse($response)">
</vn-client-postcode>
<!-- Create custom agent dialog -->
<vn-dialog class="edit"
vn-id="customAgent"
on-accept="$ctrl.onCustomAgentAccept()">
<tpl-body>
<h5 class="vn-py-sm" translate>New customs agent</h5>
<vn-horizontal>
<vn-textfield vn-one vn-focus
label="NIF"
ng-model="$ctrl.newCustomsAgent.nif"
required="true">
</vn-textfield>
<vn-textfield vn-one
label="Fiscal name"
ng-model="$ctrl.newCustomsAgent.fiscalName"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield vn-one
label="Street"
ng-model="$ctrl.newCustomsAgent.street">
</vn-textfield>
<vn-textfield vn-one
label="Phone"
ng-model="$ctrl.newCustomsAgent.phone">
</vn-textfield>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Create</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,12 +1,7 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
export default class Controller {
constructor($scope, $state) {
this.$ = $scope;
this.$state = $state;
this.$stateParams = $state.params;
}
export default class Controller extends Component {
removeObservation(index) {
this.$.watcher.setDirty();
this.$.model.remove(index);
@ -25,17 +20,26 @@ export default class Controller {
}
onSubmit() {
this.$.watcher.check();
this.$.watcher.realSubmit()
this.$.watcher.submit()
.then(() => this.$.model.save(true))
.then(() => {
this.$.watcher.notifySaved();
this.card.reload();
this.goToIndex();
});
}
showCustomAgent(event) {
if (event.defaultPrevented) return;
event.preventDefault();
this.$.customAgent.show();
}
onCustomAgentAccept() {
return this.$http.post(`CustomsAgents`, this.newCustomsAgent)
.then(res => this.address.customsAgentFk = res.data.id);
}
}
Controller.$inject = ['$scope', '$state'];
ngModule.component('vnClientAddressEdit', {
template: require('./index.html'),

View File

@ -2,15 +2,22 @@ import './index';
describe('Client', () => {
describe('Component vnClientAddressEdit', () => {
let $state;
let $scope;
let controller;
let $httpBackend;
let $element;
let $state;
beforeEach(ngModule('client'));
beforeEach(angular.mock.inject(($componentController, _$state_) => {
beforeEach(angular.mock.inject(($componentController, $rootScope, _$state_, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$state = _$state_;
$state.params.addressId = '1';
controller = $componentController('vnClientAddressEdit', {$state});
$element = angular.element('<vn-client-address-edit></vn-client-address-edit>');
controller = $componentController('vnClientAddressEdit', {$element, $scope});
controller.address = {id: 1, customsAgentFk: null};
controller.$.watcher = {
setDirty: () => {},
setPristine: () => {},
@ -55,5 +62,16 @@ describe('Client', () => {
expect(controller.$state.go).toHaveBeenCalledWith('client.card.address.index');
});
});
describe('onCustomAgentAccept()', () => {
it(`should 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();
$httpBackend.flush();
expect(controller.address.customsAgentFk).toEqual(1);
});
});
});
});

View File

@ -1,7 +0,0 @@
Enabled: Activo
Is equalizated: Recargo de equivalencia
Observation type: Tipo de observación
Description: Descripción
The observation type must be unique: El tipo de observación ha de ser único
Remove note: Quitar nota
Add note: Añadir nota

View File

@ -1,2 +0,0 @@
Set as default: Establecer como predeterminado
Active first to set as default: Active primero para marcar como predeterminado

View File

@ -0,0 +1,27 @@
# Index
Set as default: Establecer como predeterminado
Active first to set as default: Active primero para marcar como predeterminado
# Edit
Enabled: Activo
Is equalizated: Recargo de equivalencia
Observation type: Tipo de observación
Description: Descripción
The observation type must be unique: El tipo de observación ha de ser único
Remove note: Quitar nota
Add note: Añadir nota
Customs agent: Agente de aduanas
New customs agent: Nuevo agente de aduanas
# Create
Street address: Dirección postal
Default: Predeterminado
Consignee: Consignatario
Postcode: Código postal
Town/City: Ciudad
Province: Provincia
Agency: Agencia
Phone: Teléfono
Mobile: Móvil
# Common
Fiscal name: Nombre fiscal
Street: Dirección fiscal