Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 7529-workerPIT
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Carlos Satorres 2024-10-18 11:16:06 +02:00
commit 0a726a8fe7
181 changed files with 1609 additions and 5853 deletions

View File

@ -32,8 +32,7 @@ RUN apt-get update \
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
samba-common-bin samba-dsdb-modules\
&& rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2
&& rm -rf /var/lib/apt/lists/*
# Salix
@ -55,7 +54,4 @@ COPY \
README.md \
./
CMD ["pm2-runtime", "./back/process.yml"]
HEALTHCHECK --interval=15s --timeout=10s \
CMD curl -f http://localhost:3000/api/Applications/status || exit 1
CMD ["node", "--tls-min-v1.0", "--openssl-legacy-provider", "./loopback/server/server.js"]

View File

@ -19,8 +19,15 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
const [info, info2, [{'@vCollectionFk': collectionFk}]] = await Self.rawSql(
'CALL vn.collection_getAssigned(?, @vCollectionFk);SELECT @vCollectionFk', [userId], myOptions);
const randStr = Math.random().toString(36).substring(3);
const result = await Self.rawSql(`
CALL vn.collection_getAssigned(?, @vCollectionFk);
SELECT @vCollectionFk ?
`, [userId, randStr], myOptions);
const collectionFk = result.find(item => item[0]?.[randStr] !== undefined)?.[0]?.[randStr];
if (!collectionFk) throw new UserError('There are not picking tickets');
await Self.rawSql('CALL vn.collection_printSticker(?, NULL)', [collectionFk], myOptions);

View File

@ -62,7 +62,8 @@ module.exports = Self => {
p.code parkingCodePrevia,
p.pickingOrder pickingOrderPrevia,
iss.id itemShelvingSaleFk,
iss.isPicked
iss.isPicked,
iss.itemShelvingFk
FROM ticketCollection tc
LEFT JOIN collection c ON c.id = tc.collectionFk
JOIN sale s ON s.ticketFk = tc.ticketFk
@ -102,7 +103,8 @@ module.exports = Self => {
p.code,
p.pickingOrder,
iss.id itemShelvingSaleFk,
iss.isPicked
iss.isPicked,
iss.itemShelvingFk
FROM sectorCollection sc
JOIN sectorCollectionSaleGroup ss ON ss.sectorCollectionFk = sc.id
JOIN saleGroup sg ON sg.id = ss.saleGroupFk

View File

@ -4,21 +4,45 @@ module.exports = Self => {
/**
* Returns basic headers
*
* @param {string} cookie - The docuware cookie
* @return {object} - The headers
*/
Self.getOptions = async() => {
const docuwareConfig = await Self.app.models.DocuwareConfig.findOne();
const now = Date.vnNow();
let {url, username, password, token, expired} = docuwareConfig;
if (process.env.NODE_ENV && (!expired || expired < now + 60)) {
const {data: {IdentityServiceUrl}} = await axios.get(`${url}/Home/IdentityServiceInfo`);
const {data: {token_endpoint}} = await axios.get(`${IdentityServiceUrl}/.well-known/openid-configuration`);
const {data} = await axios.post(token_endpoint, {
grant_type: 'password',
scope: 'docuware.platform',
client_id: 'docuware.platform.net.client',
username,
password
}, {headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}});
const newToken = data.access_token;
token = data.token_type + ' ' + newToken;
await docuwareConfig.updateAttributes({
token,
expired: now + data.expires_in
});
}
const headers = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': docuwareConfig.cookie
'Authorization': token
}
};
return {
url: docuwareConfig.url,
url,
headers
};
};

View File

@ -2,87 +2,54 @@ const axios = require('axios');
const models = require('vn-loopback/server/server').models;
describe('Docuware core', () => {
beforeAll(() => {
const fileCabinetCode = 'deliveryNote';
beforeAll(async() => {
process.env.NODE_ENV = 'testing';
});
afterAll(() => {
delete process.env.NODE_ENV;
});
describe('getOptions()', () => {
it('should return url and headers', async() => {
const result = await models.Docuware.getOptions();
expect(result.url).toBeDefined();
expect(result.headers).toBeDefined();
const docuwareInfo = await models.Docuware.findOne({
where: {
code: fileCabinetCode
}
});
});
describe('getDialog()', () => {
it('should return dialogId', async() => {
const dialogs = {
data: {
Dialog: [
{
DisplayName: 'find',
Id: 'getDialogTest'
}
]
}
};
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
const result = await models.Docuware.getDialog('deliveryNote', 'find', 'randomFileCabinetId');
spyOn(axios, 'get').and.callFake(url => {
if (url.includes('IdentityServiceInfo')) return {data: {IdentityServiceUrl: 'IdentityServiceUrl'}};
if (url.includes('IdentityServiceUrl')) return {data: {token_endpoint: 'token_endpoint'}};
if (url.includes('dialogs')) {
return {
data: {
Dialog: [
{
DisplayName: 'find',
Id: 'getDialogTest'
}
]
}
};
}
expect(result).toEqual('getDialogTest');
});
});
describe('getFileCabinet()', () => {
it('should return fileCabinetId', async() => {
const code = 'deliveryNote';
const docuwareInfo = await models.Docuware.findOne({
where: {
code
}
});
const dialogs = {
data: {
if (url.includes('FileCabinets')) {
return {data: {
FileCabinet: [
{
Name: docuwareInfo.fileCabinetName,
Id: 'getFileCabinetTest'
}
]
}
};
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
const result = await models.Docuware.getFileCabinet(code);
expect(result).toEqual('getFileCabinetTest');
});
});
describe('get()', () => {
it('should return data without parse', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
id: 1
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const result = await models.Docuware.get('deliveryNote');
expect(result.id).toEqual(1);
}};
}
});
it('should return data with parse', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
spyOn(axios, 'post').and.callFake(url => {
if (url.includes('token_endpoint')) {
return {data: {
access_token: 'access_token',
token_type: 'bearer',
expires_in: 10000
}};
}
if (url.includes('DialogExpression')) {
return {data: {
Items: [{
Fields: [
{
@ -103,12 +70,52 @@ describe('Docuware core', () => {
]
}]
}
};
};
}
});
});
afterAll(() => {
delete process.env.NODE_ENV;
});
describe('getOptions()', () => {
it('should return url and headers', async() => {
const result = await models.Docuware.getOptions();
expect(result.url).toBeDefined();
expect(result.headers).toBeDefined();
});
});
describe('Dialog()', () => {
it('should return dialogId', async() => {
const result = await models.Docuware.getDialog('deliveryNote', 'find', 'randomFileCabinetId');
expect(result).toEqual('getDialogTest');
});
});
describe('getFileCabinet()', () => {
it('should return fileCabinetId', async() => {
const result = await models.Docuware.getFileCabinet(fileCabinetCode);
expect(result).toEqual('getFileCabinetTest');
});
});
describe('get()', () => {
it('should return data without parse', async() => {
const [result] = await models.Docuware.get('deliveryNote');
expect(result.firstRequiredField).toEqual(1);
});
it('should return data with parse', async() => {
const parse = {
'firstRequiredField': 'id',
'secondRequiredField': 'name',
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const [result] = await models.Docuware.get('deliveryNote', null, parse);
expect(result.id).toEqual(1);
@ -119,17 +126,14 @@ describe('Docuware core', () => {
describe('getById()', () => {
it('should return data', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
id: 1
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const result = await models.Docuware.getById('deliveryNote', 1);
spyOn(models.Docuware, 'get');
await models.Docuware.getById('deliveryNote', 1);
expect(result.id).toEqual(1);
expect(models.Docuware.get).toHaveBeenCalledWith(
'deliveryNote',
{condition: [Object({DBName: 'N__ALBAR_N', Value: [1]})]},
undefined
);
});
});
});

View File

@ -143,7 +143,7 @@ module.exports = Self => {
headers: {
'Content-Type': 'multipart/form-data',
'X-File-ModifiedDate': Date.vnNew(),
'Cookie': docuwareOptions.headers.headers.Cookie,
'Authorization': docuwareOptions.headers.headers.Authorization,
...data.getHeaders()
},
};

View File

@ -72,9 +72,9 @@ describe('Renew Token', () => {
}
expect(error).toBeDefined();
const query = 'SELECT * FROM util.debug';
const debugLog = await models.Application.rawSql(query, null);
const query = 'SELECT * FROM util.debug WHERE variable = "renewToken"';
const debugLog = await models.Application.rawSql(query);
expect(debugLog.length).toEqual(1);
});

View File

@ -175,6 +175,9 @@
"PrintConfig": {
"dataSource": "vn"
},
"QueueMember": {
"dataSource": "vn"
},
"ViaexpressConfig": {
"dataSource": "vn"
},

View File

@ -16,17 +16,17 @@
"url": {
"type": "string"
},
"cookie": {
"token": {
"type": "string"
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"expired":{
"type": "number"
}
},
"acls": [
{
"property": "*",
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}
}

View File

@ -0,0 +1,38 @@
{
"name": "QueueMember",
"base": "VnModel",
"options": {
"mysql": {
"table": "pbx.queueMember"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"queue": {
"type": "string"
},
"extension": {
"type": "string"
}
},
"relations": {
"queueRelation": {
"type": "belongsTo",
"model": "Queue",
"foreignKey": "queue",
"primaryKey": "name"
}
},
"acls": [
{
"property": "*",
"accessType": "READ",
"principalType": "ROLE",
"principalId": "employee",
"permission": "ALLOW"
}
]
}

View File

@ -1,7 +0,0 @@
apps:
- script: ./loopback/server/server.js
name: salix-back
instances: 1
max_restarts: 0
autorestart: false
node_args: --tls-min-v1.0 --openssl-legacy-provider

View File

@ -185,6 +185,7 @@ INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory
(3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0),
(4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1),
(5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0),
(6, 'Warehouse six', 'vnh', 1, 1, 1, 1, 0, 0, 1, 1, 0, 0),
(13, 'Inventory', 'inv', 1, 1, 1, 0, 0, 0, 1, 0, 0, 0),
(60, 'Algemesi', NULL, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0);
@ -387,23 +388,23 @@ INSERT INTO `vn`.`contactChannel`(`id`, `name`)
(4, 'GCN Channel'),
(5, 'The Newspaper');
INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city`,`postcode`,`phone`,`mobile`,`isRelevant`,`email`,`iban`,`dueDay`,`accountingAccount`,`isEqualizated`,`provinceFk`,`hasToInvoice`,`credit`,`countryFk`,`isActive`,`gestdocFk`,`quality`,`payMethodFk`,`created`,`isToBeMailed`,`contactChannelFk`,`hasSepaVnl`,`hasCoreVnl`,`hasCoreVnh`,`riskCalculated`, `hasToInvoiceByAddress`,`isTaxDataChecked`,`isFreezed`,`creditInsurance`,`isCreatedAsServed`,`hasInvoiceSimplified`,`salesPersonFk`,`isVies`,`businessTypeFk`,`typeFk`)
INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city`,`postcode`,`phone`,`mobile`,`isRelevant`,`email`,`iban`,`dueDay`,`accountingAccount`,`isEqualizated`,`provinceFk`,`hasToInvoice`,`credit`,`countryFk`,`isActive`,`quality`,`payMethodFk`,`created`,`isToBeMailed`,`contactChannelFk`,`hasSepaVnl`,`hasCoreVnl`,`hasCoreVnh`,`riskCalculated`, `hasToInvoiceByAddress`,`isTaxDataChecked`,`isFreezed`,`creditInsurance`,`isCreatedAsServed`,`hasInvoiceSimplified`,`salesPersonFk`,`isVies`,`businessTypeFk`,`typeFk`)
VALUES
(1101, 'Bruce Wayne', '84612325V', 'BATMAN', 'Alfred', '1007 MOUNTAIN DRIVE, GOTHAM', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceWayne@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1102, 'Petter Parker', '87945234L', 'SPIDER MAN', 'Aunt May', '20 INGRAM STREET, QUEENS, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 2, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1103, 'Clark Kent', '06815934E', 'SUPER MAN', 'lois lane', '344 CLINTON STREET, APARTAMENT 3-D', 'Gotham', 46460, 1111111111, 222222222, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 0, 19, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1104, 'Tony Stark', '06089160W', 'IRON MAN', 'Pepper Potts', '10880 MALIBU POINT, 90265', 'Gotham', 46460, 1111111111, 222222222, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 2, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1105, 'Max Eisenhardt', '251628698', 'MAGNETO', 'Rogue', 'UNKNOWN WHEREABOUTS', 'Gotham', 46460, 1111111111, 222222222, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 300, 8, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, NULL, 0, 0, 18, 0, 'florist','normal'),
(1106, 'DavidCharlesHaller', '53136686Q', 'LEGION', 'Charles Xavier', 'CITY OF NEW YORK, NEW YORK, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 19, 0, 'florist','normal'),
(1107, 'Hank Pym', '09854837G', 'ANT MAN', 'Hawk', 'ANTHILL, SAN FRANCISCO, CALIFORNIA', 'Gotham', 46460, 1111111111, 222222222, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 0, 0, NULL, 0, 0, 19, 0, 'florist','normal'),
(1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 5, 1, 300, 13, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, NULL, 0, 0, 19, 0, 'florist','normal'),
(1109, 'Bruce Banner', '16104829E', 'HULK', 'Black widow', 'SOMEWHERE IN NEW YORK', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 0, 0, NULL, 0, 0, 9, 0, 'florist','normal'),
(1110, 'Jessica Jones', '58282869H', 'JESSICA JONES', 'Luke Cage', 'NYCC 2015 POSTER', 'Gotham', 46460, 1111111111, 222222222, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, NULL, 1, 'florist','normal'),
(1111, 'Missing', NULL, 'MISSING MAN', 'Anton', 'THE SPACE, UNIVERSE FAR AWAY', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 0, 1, 0, NULL, 1, 0, NULL, 0, 'others','loses'),
(1112, 'Trash', NULL, 'GARBAGE MAN', 'Unknown name', 'NEW YORK CITY, UNDERGROUND', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 0, 1, 0, NULL, 1, 0, NULL, 0, 'others','loses');
(1101, 'Bruce Wayne', '84612325V', 'BATMAN', 'Alfred', '1007 MOUNTAIN DRIVE, GOTHAM', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceWayne@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1102, 'Petter Parker', '87945234L', 'SPIDER MAN', 'Aunt May', '20 INGRAM STREET, QUEENS, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 2, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1103, 'Clark Kent', '06815934E', 'SUPER MAN', 'lois lane', '344 CLINTON STREET, APARTAMENT 3-D', 'Gotham', 46460, 1111111111, 222222222, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 0, 19, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1104, 'Tony Stark', '06089160W', 'IRON MAN', 'Pepper Potts', '10880 MALIBU POINT, 90265', 'Gotham', 46460, 1111111111, 222222222, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 2, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 18, 0, 'florist','normal'),
(1105, 'Max Eisenhardt', '251628698', 'MAGNETO', 'Rogue', 'UNKNOWN WHEREABOUTS', 'Gotham', 46460, 1111111111, 222222222, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 300, 8, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, NULL, 0, 0, 18, 0, 'florist','normal'),
(1106, 'DavidCharlesHaller', '53136686Q', 'LEGION', 'Charles Xavier', 'CITY OF NEW YORK, NEW YORK, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, 19, 0, 'florist','normal'),
(1107, 'Hank Pym', '09854837G', 'ANT MAN', 'Hawk', 'ANTHILL, SAN FRANCISCO, CALIFORNIA', 'Gotham', 46460, 1111111111, 222222222, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 0, 0, NULL, 0, 0, 19, 0, 'florist','normal'),
(1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 5, 1, 300, 13, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, NULL, 0, 0, 19, 0, 'florist','normal'),
(1109, 'Bruce Banner', '16104829E', 'HULK', 'Black widow', 'SOMEWHERE IN NEW YORK', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 0, 0, NULL, 0, 0, 9, 0, 'florist','normal'),
(1110, 'Jessica Jones', '58282869H', 'JESSICA JONES', 'Luke Cage', 'NYCC 2015 POSTER', 'Gotham', 46460, 1111111111, 222222222, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, NULL, 0, 0, NULL, 1, 'florist','normal'),
(1111, 'Missing', NULL, 'MISSING MAN', 'Anton', 'THE SPACE, UNIVERSE FAR AWAY', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 0, 1, 0, NULL, 1, 0, NULL, 0, 'others','loses'),
(1112, 'Trash', NULL, 'GARBAGE MAN', 'Unknown name', 'NEW YORK CITY, UNDERGROUND', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 0, 1, 0, NULL, 1, 0, NULL, 0, 'others','loses');
INSERT INTO `vn`.`client`(`id`, `name`, `fi`, `socialName`, `contact`, `street`, `city`, `postcode`, `isRelevant`, `email`, `iban`,`dueDay`,`accountingAccount`, `isEqualizated`, `provinceFk`, `hasToInvoice`, `credit`, `countryFk`, `isActive`, `gestdocFk`, `quality`, `payMethodFk`,`created`, `isTaxDataChecked`)
SELECT id, name, CONCAT(RPAD(CONCAT(id,9),8,id),'A'), UPPER(CONCAT(name, 'Social')), CONCAT(name, 'Contact'), CONCAT(name, 'Street'), 'GOTHAM', 46460, 1, CONCAT(name,'@mydomain.com'), NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1,NULL, 10, 5, util.VN_CURDATE(), 1
INSERT INTO `vn`.`client`(`id`, `name`, `fi`, `socialName`, `contact`, `street`, `city`, `postcode`, `isRelevant`, `email`, `iban`,`dueDay`,`accountingAccount`, `isEqualizated`, `provinceFk`, `hasToInvoice`, `credit`, `countryFk`, `isActive`, `quality`, `payMethodFk`,`created`, `isTaxDataChecked`)
SELECT id, name, CONCAT(RPAD(CONCAT(id,9),8,id),'A'), UPPER(CONCAT(name, 'Social')), CONCAT(name, 'Contact'), CONCAT(name, 'Street'), 'GOTHAM', 46460, 1, CONCAT(name,'@mydomain.com'), NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, 10, 5, util.VN_CURDATE(), 1
FROM `account`.`role` `r`
WHERE `r`.`hasLogin` = 1;
@ -545,7 +546,8 @@ INSERT INTO `vn`.`observationType`(`id`,`description`, `code`)
(6, 'Weight', 'weight'),
(7, 'InvoiceOut', 'invoiceOut'),
(8, 'DropOff', 'dropOff'),
(9, 'Sustitución', 'substitution');
(9, 'Sustitución', 'substitution'),
(10, 'Finance', 'finance');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES
@ -3941,6 +3943,11 @@ INSERT INTO vn.medicalReview
(id, workerFk, centerFk, `date`, `time`, isFit, amount, invoice, remark)
VALUES(3, 9, 2, '2000-01-01', '8:00', 1, 150.0, NULL, NULL);
INSERT INTO vn.stockBought (workerFk, bought, reserve, dated)
VALUES(35, 1.00, 1.00, '2001-01-01');
INSERT INTO vn.auctionConfig (id,conversionCoefficient,warehouseFk)
VALUES (1,0.6,6);
INSERT INTO vn.payrollComponent
(id, name, isSalaryAgreed, isVariable, isException)
VALUES
@ -3982,3 +3989,25 @@ VALUES
INSERT IGNORE INTO ormConfig
SET id =1,
selectLimit = 1000;
INSERT INTO pbx.queueMultiConfig
SET id = 'ring',
strategy = 20,
timeout = 2,
retry = 0,
weight = 0,
maxLen = 0,
ringInUse = 0;
INSERT INTO pbx.queue (description, name, config)
VALUES ('X-men', '1000', 1),
('Avengers', '2000', 1);
INSERT IGNORE INTO pbx.queueMember
SET queue = '1000',
extension = '1010';
UPDATE vn.department SET pbxQueue = '1000' WHERE name = "CAMARA";
UPDATE vn.department SET pbxQueue = '2000' WHERE name = "VENTAS";

View File

@ -1,52 +1,52 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost`
PROCEDURE `hedera`.`orderRow_updateOverstocking`(vOrderFk INT)
BEGIN
/**
* Set amount = 0 to avoid overbooking sales
*
* @param vOrderFk hedera.order.id
*/
DECLARE vCalcFk INT;
DECLARE vDone BOOL;
DECLARE vWarehouseFk INT;
DECLARE cWarehouses CURSOR FOR
SELECT DISTINCT warehouseFk
FROM orderRow
WHERE orderFk = vOrderFk
AND shipped = util.VN_CURDATE();
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
OPEN cWarehouses;
checking: LOOP
SET vDone = FALSE;
FETCH cWarehouses INTO vWarehouseFk;
IF vDone THEN
LEAVE checking;
END IF;
CALL cache.available_refresh(vCalcFk, FALSE, vWarehouseFk, util.VN_CURDATE());
UPDATE orderRow r
JOIN `order` o ON o.id = r.orderFk
JOIN orderConfig oc
JOIN cache.available a ON a.calc_id = vCalcFk AND a.item_id = r.itemFk
SET r.amount = 0
WHERE ADDTIME(o.rowUpdated, oc.reserveTime) < util.VN_NOW()
AND a.available <= 0
AND r.warehouseFk = vWarehouseFk
AND r.orderFk = vOrderFk;
END LOOP;
CLOSE cWarehouses;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost`
PROCEDURE `hedera`.`orderRow_updateOverstocking`(vOrderFk INT)
BEGIN
/**
* Set amount = 0 to avoid overbooking sales
*
* @param vOrderFk hedera.order.id
*/
DECLARE vCalcFk INT;
DECLARE vDone BOOL;
DECLARE vWarehouseFk INT;
DECLARE cWarehouses CURSOR FOR
SELECT DISTINCT warehouseFk
FROM orderRow
WHERE orderFk = vOrderFk
AND shipment = util.VN_CURDATE();
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
OPEN cWarehouses;
checking: LOOP
SET vDone = FALSE;
FETCH cWarehouses INTO vWarehouseFk;
IF vDone THEN
LEAVE checking;
END IF;
CALL cache.available_refresh(vCalcFk, FALSE, vWarehouseFk, util.VN_CURDATE());
UPDATE orderRow r
JOIN `order` o ON o.id = r.orderFk
JOIN orderConfig oc
JOIN cache.available a ON a.calc_id = vCalcFk AND a.item_id = r.itemFk
SET r.amount = 0
WHERE ADDTIME(o.rowUpdated, oc.reserveTime) < util.VN_NOW()
AND a.available <= 0
AND r.warehouseFk = vWarehouseFk
AND r.orderFk = vOrderFk;
END LOOP;
CLOSE cWarehouses;
END$$
DELIMITER ;

View File

@ -22,7 +22,6 @@ BEGIN
DECLARE vItemFk INT;
DECLARE vConcept VARCHAR(30);
DECLARE vAmount INT;
DECLARE vAvailable INT;
DECLARE vPrice DECIMAL(10,2);
DECLARE vSaleFk INT;
DECLARE vRowFk INT;
@ -32,7 +31,6 @@ BEGIN
DECLARE vClientFk INT;
DECLARE vCompanyFk INT;
DECLARE vAgencyModeFk INT;
DECLARE vCalcFk INT;
DECLARE vIsTaxDataChecked BOOL;
DECLARE vDates CURSOR FOR
@ -98,22 +96,8 @@ BEGIN
SELECT employeeFk INTO vUserFk FROM orderConfig;
END IF;
START TRANSACTION;
CALL order_checkEditable(vSelf);
CALL orderRow_updateOverstocking(vSelf);
-- Check order is not empty
SELECT COUNT(*) > 0 INTO vHasRows
FROM orderRow
WHERE orderFk = vSelf
AND amount > 0;
IF NOT vHasRows THEN
CALL util.throw('ORDER_EMPTY');
END IF;
-- Check if any product has a quantity of 0
SELECT EXISTS (
SELECT id
@ -123,7 +107,21 @@ BEGIN
) INTO vHas0Amount;
IF vHas0Amount THEN
CALL util.throw('Remove lines with quantity = 0 before confirming');
CALL util.throw('Hay líneas vacías. Por favor, elimínelas');
END IF;
START TRANSACTION;
CALL order_checkEditable(vSelf);
-- Check order is not empty
SELECT COUNT(*) > 0 INTO vHasRows
FROM orderRow
WHERE orderFk = vSelf
AND amount > 0;
IF NOT vHasRows THEN
CALL util.throw('ORDER_EMPTY');
END IF;
-- Crea los tickets del pedido

View File

@ -1,10 +1,10 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `hedera`.`orderRow_afterInsert`
AFTER INSERT ON `orderRow`
FOR EACH ROW
BEGIN
UPDATE `order`
SET rowUpdated = NOW()
WHERE id = NEW.orderFk;
END$$
DELIMITER ;
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `hedera`.`orderRow_afterInsert`
AFTER INSERT ON `orderRow`
FOR EACH ROW
BEGIN
UPDATE `order`
SET rowUpdated = util.VN_NOW()
WHERE id = NEW.orderFk;
END$$
DELIMITER ;

View File

@ -23,6 +23,13 @@ BEGIN
DELETE FROM messageInbox WHERE sendDate < v2Months;
DELETE FROM messageInbox WHERE sendDate < v2Months;
DELETE FROM workerTimeControl WHERE timed < v4Years;
DELETE FROM itemShelvingSale
WHERE itemShelvingFk IN (
SELECT id
FROM itemShelving
WHERE created < util.VN_CURDATE()
AND visible = 0
);
DELETE FROM itemShelving WHERE created < util.VN_CURDATE() AND visible = 0;
DELETE FROM ticketDown WHERE created < util.yesterday();
DELETE IGNORE FROM expedition WHERE created < v26Months;

View File

@ -3,7 +3,7 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`collection_assign`(
vUserFk INT,
OUT vCollectionFk INT
)
BEGIN
BEGIN
/**
* Comprueba si existen colecciones libres que se ajustan
* al perfil del usuario y le asigna la más antigua.
@ -45,6 +45,12 @@ BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
-- Si hay colecciones sin terminar, sale del proceso
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
CALL collection_get(vUserFk);
SELECT (pc.maxNotReadyCollections - COUNT(*)) <= 0, pc.maxNotAssignedCollectionLifeTime
@ -118,9 +124,19 @@ BEGIN
IF vCollectionFk IS NULL THEN
CALL collection_new(vUserFk, vCollectionFk);
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
START TRANSACTION;
SELECT workerFk INTO vCollectionWorker
FROM `collection`
WHERE id = vCollectionFk FOR UPDATE;
IF vCollectionWorker IS NULL THEN
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
END IF;
COMMIT;
END IF;
END$$
DELIMITER ;

View File

@ -5,100 +5,139 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`collection_getAssigne
)
BEGIN
/**
* Comprueba si existen colecciones libres que se ajustan al perfil del usuario
* y le asigna la más antigua.
* Añade un registro al semillero de colecciones y hace la reserva para la colección
*
* Comprueba si existen colecciones libres que se ajustan
* al perfil del usuario y le asigna la más antigua.
* Añade un registro al semillero de colecciones.
*
* @param vUserFk Id de usuario
* @param vCollectionFk Id de colección
*/
DECLARE vHasTooMuchCollections BOOL;
DECLARE vItemPackingTypeFk VARCHAR(1);
DECLARE vWarehouseFk INT;
DECLARE vLockName VARCHAR(215);
DECLARE vLockTime INT DEFAULT 30;
DECLARE vDone BOOL DEFAULT FALSE;
DECLARE vCollectionWorker INT;
DECLARE vMaxNotAssignedCollectionLifeTime TIME;
DECLARE vCollections CURSOR FOR
WITH collections AS (
SELECT tc.collectionFk,
SUM(sv.volume) volume,
c.saleTotalCount,
c.itemPackingTypeFk,
c.trainFk,
c.warehouseFk,
c.wagons
FROM vn.ticketCollection tc
JOIN vn.collection c ON c.id = tc.collectionFk
JOIN vn.saleVolume sv ON sv.ticketFk = tc.ticketFk
WHERE c.workerFk IS NULL
AND sv.shipped >= util.VN_CURDATE()
GROUP BY tc.collectionFk
) SELECT c.collectionFk
FROM collections c
JOIN vn.operator o
WHERE o.workerFk = vUserFk
AND (c.saleTotalCount <= o.linesLimit OR o.linesLimit IS NULL)
AND (c.itemPackingTypeFk = o.itemPackingTypeFk OR o.itemPackingTypeFk IS NULL)
AND o.numberOfWagons = c.wagons
AND o.trainFk = c.trainFk
AND o.warehouseFk = c.warehouseFk;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
-- Si hay colecciones sin terminar, sale del proceso
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
IF vLockName IS NOT NULL THEN
DO RELEASE_LOCK(vLockName);
END IF;
ROLLBACK;
RESIGNAL;
END;
-- Si hay colecciones sin terminar, sale del proceso
CALL collection_get(vUserFk);
SELECT (pc.maxNotReadyCollections - COUNT(*)) <= 0,
pc.collection_assign_lockname
INTO vHasTooMuchCollections,
vLockName
FROM tmp.collection c
JOIN productionConfig pc;
SELECT (pc.maxNotReadyCollections - COUNT(*)) <= 0, pc.maxNotAssignedCollectionLifeTime
INTO vHasTooMuchCollections, vMaxNotAssignedCollectionLifeTime
FROM productionConfig pc
LEFT JOIN tmp.collection ON TRUE;
DROP TEMPORARY TABLE tmp.collection;
IF vHasTooMuchCollections THEN
CALL util.throw('There are pending collections');
END IF;
SELECT warehouseFk, itemPackingTypeFk
INTO vWarehouseFk, vItemPackingTypeFk
FROM operator
WHERE workerFk = vUserFk;
SET vLockName = CONCAT_WS('/',
vLockName,
vWarehouseFk,
vItemPackingTypeFk
);
IF NOT GET_LOCK(vLockName, vLockTime) THEN
CALL util.throw(CONCAT('Cannot get lock: ', vLockName));
CALL util.throw('Hay colecciones pendientes');
END IF;
-- Se eliminan las colecciones sin asignar que estan obsoletas
INSERT INTO ticketTracking(stateFk, ticketFk)
SELECT s.id, tc.ticketFk
FROM collection c
JOIN ticketCollection tc ON tc.collectionFk = c.id
JOIN state s ON s.code = 'PRINTED_AUTO'
JOIN productionConfig pc
WHERE c.workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), c.created) > pc.maxNotAssignedCollectionLifeTime;
DELETE c
FROM collection c
JOIN productionConfig pc
WHERE c.workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), c.created) > pc.maxNotAssignedCollectionLifeTime;
INSERT INTO ticketTracking(stateFk, ticketFk)
SELECT s.id, tc.ticketFk
FROM `collection` c
JOIN ticketCollection tc ON tc.collectionFk = c.id
JOIN `state` s ON s.code = 'PRINTED_AUTO'
WHERE c.workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), c.created) > vMaxNotAssignedCollectionLifeTime;
DELETE FROM `collection`
WHERE workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), created) > vMaxNotAssignedCollectionLifeTime;
-- Se añade registro al semillero
INSERT INTO collectionHotbed
SET userFk = vUserFk;
INSERT INTO collectionHotbed(userFk) VALUES(vUserFk);
-- Comprueba si hay colecciones disponibles que se ajustan a su configuracion
SELECT MIN(c.id) INTO vCollectionFk
FROM collection c
JOIN operator o ON (o.itemPackingTypeFk = c.itemPackingTypeFk
OR c.itemPackingTypeFk IS NULL)
AND o.numberOfWagons = c.wagons
AND o.trainFk = c.trainFk
AND o.warehouseFk = c.warehouseFk
AND c.workerFk IS NULL
WHERE o.workerFk = vUserFk;
OPEN vCollections;
l: LOOP
SET vDone = FALSE;
FETCH vCollections INTO vCollectionFk;
IF vDone THEN
LEAVE l;
END IF;
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET vCollectionFk = NULL;
RESIGNAL;
END;
START TRANSACTION;
SELECT workerFk INTO vCollectionWorker
FROM `collection`
WHERE id = vCollectionFk FOR UPDATE;
IF vCollectionWorker IS NULL THEN
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
COMMIT;
LEAVE l;
END IF;
ROLLBACK;
END;
END LOOP;
CLOSE vCollections;
IF vCollectionFk IS NULL THEN
CALL collection_new(vUserFk, vCollectionFk);
START TRANSACTION;
SELECT workerFk INTO vCollectionWorker
FROM `collection`
WHERE id = vCollectionFk FOR UPDATE;
IF vCollectionWorker IS NULL THEN
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
END IF;
COMMIT;
END IF;
UPDATE collection
SET workerFk = vUserFk
WHERE id = vCollectionFk;
CALL itemShelvingSale_addByCollection(vCollectionFk);
DO RELEASE_LOCK(vLockName);
END$$
DELIMITER ;

View File

@ -45,7 +45,7 @@ BEGIN
LEFT JOIN observation ob ON ob.ticketFk = t.id
WHERE t.id = vParamFk
AND t.shipped >= vYesterday
UNION ALL
UNION
SELECT t.id ticketFk,
IF(NOT(vItemPackingTypeFk <=> 'V'), cc.code, CONCAT(SUBSTRING('ABCDEFGH', tc.wagon, 1), '-', tc.`level`)) `level`,
am.name agencyName,
@ -66,7 +66,7 @@ BEGIN
LEFT JOIN vn.worker w ON w.id = c.salesPersonFk
LEFT JOIN observation ob ON ob.ticketFk = t.id
WHERE tc.collectionFk = vParamFk
UNION ALL
UNION
SELECT sg.ticketFk,
NULL `level`,
am.name agencyName,
@ -83,6 +83,7 @@ BEGIN
LEFT JOIN observation ob ON ob.ticketFk = t.id
LEFT JOIN vn.client c ON c.id = t.clientFk
WHERE sc.id = vParamFk
AND t.shipped >= vYesterday;
AND t.shipped >= vYesterday
GROUP BY ticketFk;
END$$
DELIMITER ;

View File

@ -5,22 +5,26 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`expeditionPallet_buil
vWorkerFk INT,
OUT vPalletFk INT
)
BEGIN
/** Construye un pallet de expediciones.
proc: BEGIN
/**
* Builds an expedition pallet.
*
* Primero comprueba si esas expediciones ya pertenecen a otro pallet,
* en cuyo caso actualiza ese pallet.
* First, it checks if these expeditions already belong to another pallet,
* in which case it returns an error.
*
* @param vExpeditions JSON_ARRAY con esta estructura [exp1, exp2, exp3, ...]
* @param vArcId INT Identificador de arcRead
* @param vWorkerFk INT Identificador de worker
* @param out vPalletFk Identificador de expeditionPallet
* @param vExpeditions JSON_ARRAY with this structure [exp1, exp2, exp3, ...]
* @param vArcId INT Identifier of arcRead
* @param vWorkerFk INT Identifier of worker
* @param out vPalletFk Identifier of expeditionPallet
*/
DECLARE vCounter INT;
DECLARE vExpeditionFk INT;
DECLARE vTruckFk INT;
DECLARE vPrinterFk INT;
DECLARE vExpeditionStateTypeFk INT;
DECLARE vFreeExpeditionCount INT;
DECLARE vExpeditionWithPallet INT;
CREATE OR REPLACE TEMPORARY TABLE tExpedition (
expeditionFk INT,
@ -44,48 +48,63 @@ BEGIN
WHERE e.id = vExpeditionFk;
END WHILE;
SELECT palletFk INTO vPalletFk
FROM (
SELECT palletFk, count(*) n
FROM tExpedition
WHERE palletFk > 0
GROUP BY palletFk
ORDER BY n DESC
LIMIT 100
) sub
LIMIT 1;
SELECT COUNT(expeditionFk) INTO vFreeExpeditionCount
FROM tExpedition
WHERE palletFk IS NULL;
IF vPalletFk IS NULL THEN
SELECT roadmapStopFk INTO vTruckFk
FROM (
SELECT rm.roadmapStopFk, count(*) n
FROM routesMonitor rm
JOIN tExpedition e ON e.routeFk = rm.routeFk
GROUP BY roadmapStopFk
ORDER BY n DESC
LIMIT 1
) sub;
SELECT COUNT(expeditionFk) INTO vExpeditionWithPallet
FROM tExpedition
WHERE palletFk;
IF vTruckFk IS NULL THEN
CALL util.throw ('TRUCK_NOT_AVAILABLE');
END IF;
INSERT INTO expeditionPallet SET truckFk = vTruckFk;
SET vPalletFk = LAST_INSERT_ID();
IF vExpeditionWithPallet THEN
UPDATE arcRead
SET error = (
SELECT GROUP_CONCAT(expeditionFk SEPARATOR ', ')
FROM tExpedition
WHERE palletFk
)
WHERE id = vArcId;
LEAVE proc;
END IF;
IF NOT vFreeExpeditionCount THEN
CALL util.throw ('NO_FREE_EXPEDITIONS');
END IF;
SELECT roadmapStopFk INTO vTruckFk
FROM (
SELECT rm.roadmapStopFk, count(*) n
FROM routesMonitor rm
JOIN tExpedition e ON e.routeFk = rm.routeFk
WHERE e.palletFk IS NULL
GROUP BY roadmapStopFk
ORDER BY n DESC
LIMIT 1
) sub;
IF vTruckFk IS NULL THEN
CALL util.throw ('TRUCK_NOT_AVAILABLE');
END IF;
INSERT INTO expeditionPallet SET truckFk = vTruckFk;
SET vPalletFk = LAST_INSERT_ID();
INSERT INTO expeditionScan(expeditionFk, palletFk, workerFk)
SELECT expeditionFk, vPalletFk, vWorkerFk
FROM tExpedition
ON DUPLICATE KEY UPDATE palletFk = vPalletFk, workerFk = vWorkerFk;
WHERE palletFk IS NULL;
SELECT id INTO vExpeditionStateTypeFk
FROM expeditionStateType
WHERE code = 'PALLETIZED';
INSERT INTO expeditionState(expeditionFk, typeFk)
SELECT expeditionFk, vExpeditionStateTypeFk FROM tExpedition;
SELECT expeditionFk, vExpeditionStateTypeFk
FROM tExpedition
WHERE palletFk IS NULL;
UPDATE arcRead SET error = NULL WHERE id = vArcId;
SELECT printerFk INTO vPrinterFk FROM arcRead WHERE id = vArcId;

View File

@ -0,0 +1,49 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`itemShelvingSale_deleteAdded`(
vSelf INT(11)
)
proc: BEGIN
/**
* Borra una reservea devolviendo la cantidad al itemShelving
*
* @param vSelf Identificador del itemShelvingSale
*/
DECLARE vSaleFk INT;
DECLARE vHasSalesPicked BOOL;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SELECT iss.saleFk INTO vSaleFk
FROM itemShelvingSale iss
JOIN sale s ON s.id = iss.saleFk
WHERE iss.id = vSelf AND s.isAdded
FOR UPDATE;
IF vSaleFk IS NULL THEN
CALL util.throw('The sale can not be deleted');
END IF;
SELECT COUNT(*) INTO vHasSalesPicked
FROM itemShelvingSale
WHERE saleFk = vSaleFk AND isPicked;
IF vHasSalesPicked THEN
CALL util.throw('A sale with picked sales cannot be deleted');
END IF;
UPDATE itemShelvingSale iss
JOIN itemShelving ish ON ish.id = iss.itemShelvingFk
SET ish.available = ish.available + iss.quantity
WHERE iss.saleFk = vSaleFk;
DELETE FROM sale WHERE id = vSaleFk;
COMMIT;
END$$
DELIMITER ;

View File

@ -8,17 +8,18 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`item_getSimilar`(
)
BEGIN
/**
* Propone articulos ordenados, con la cantidad
* de veces usado y segun sus caracteristicas.
*
* @param vSelf Id de artículo
* @param vWarehouseFk Id de almacen
* @param vDated Fecha
* @param vShowType Mostrar tipos
* @param vDaysInForward Días de alcance para las ventas
*/
* Propone articulos ordenados, con la cantidad
* de veces usado y segun sus caracteristicas.
*
* @param vSelf Id de artículo
* @param vWarehouseFk Id de almacen
* @param vDated Fecha
* @param vShowType Mostrar tipos
* @param vDaysInForward Días de alcance para las ventas (https://redmine.verdnatura.es/issues/7956#note-4)
*/
DECLARE vAvailableCalcFk INT;
DECLARE vVisibleCalcFk INT;
DECLARE vTypeFk INT;
DECLARE vPriority INT DEFAULT 1;
CALL cache.available_refresh(vAvailableCalcFk, FALSE, vWarehouseFk, vDated);
@ -42,19 +43,9 @@ BEGIN
AND it.priority = vPriority
LEFT JOIN vn.tag t ON t.id = it.tagFk
WHERE i.id = vSelf
),
sold AS (
SELECT SUM(s.quantity) quantity, s.itemFk
FROM vn.sale s
JOIN vn.ticket t ON t.id = s.ticketFk
LEFT JOIN vn.itemShelvingSale iss ON iss.saleFk = s.id
WHERE t.shipped >= CURDATE() + INTERVAL vDaysInForward DAY
AND iss.saleFk IS NULL
AND t.warehouseFk = vWarehouseFk
GROUP BY s.itemFk
)
SELECT i.id itemFk,
LEAST(CAST(sd.quantity AS INT), v.visible) advanceable,
NULL advanceable, -- https://redmine.verdnatura.es/issues/7956#note-4
i.longName,
i.subName,
i.tag5,
@ -79,7 +70,6 @@ BEGIN
v.visible located,
b.price2
FROM vn.item i
LEFT JOIN sold sd ON sd.itemFk = i.id
JOIN cache.available a ON a.item_id = i.id
AND a.calc_id = vAvailableCalcFk
LEFT JOIN cache.visible v ON v.item_id = i.id
@ -93,21 +83,20 @@ BEGIN
LEFT JOIN vn.tag t ON t.id = it.tagFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
JOIN itemTags its
WHERE (a.available > 0 OR sd.quantity < v.visible)
WHERE a.available > 0
AND (i.typeFk = its.typeFk OR NOT vShowType)
AND i.id <> vSelf
ORDER BY (a.available > 0) DESC,
`counter` DESC,
(t.name = its.name) DESC,
(it.value = its.value) DESC,
(i.tag5 = its.tag5) DESC,
match5 DESC,
(i.tag6 = its.tag6) DESC,
match6 DESC,
(i.tag7 = its.tag7) DESC,
match7 DESC,
(i.tag8 = its.tag8) DESC,
match8 DESC
ORDER BY `counter` DESC,
(t.name = its.name) DESC,
(it.value = its.value) DESC,
(i.tag5 = its.tag5) DESC,
match5 DESC,
(i.tag6 = its.tag6) DESC,
match6 DESC,
(i.tag7 = its.tag7) DESC,
match7 DESC,
(i.tag8 = its.tag8) DESC,
match8 DESC
LIMIT 100;
END$$
DELIMITER ;

View File

@ -0,0 +1,29 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`queueMember_updateQueue`(
vBusinessFk INT
)
BEGIN
/**
* Elimina la entrada de la cola anterior y luego inserta la nueva para un trabajador.
*
* @param vBusinessFk ID del negocio
*/
DECLARE vNewQueue VARCHAR(10);
DECLARE vExtension VARCHAR(10);
DECLARE exit handler FOR SQLEXCEPTION
SELECT d.pbxQueue, s.extension
INTO vNewQueue, vExtension
FROM business b
JOIN department d ON d.id = b.departmentFk
JOIN pbx.sip s ON s.user_id = b.workerFk
WHERE b.id = vBusinessFk;
DELETE FROM pbx.queueMember
WHERE extension = vExtension COLLATE utf8_general_ci;
INSERT IGNORE INTO pbx.queueMember (queue, extension)
VALUES (vNewQueue, vExtension);
END$$
DELIMITER ;

View File

@ -0,0 +1,20 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`saleTracking_sectorCollectionAddPrevOK`(
vSectorCollectionFk INT
)
BEGIN
/**
* Inserta los registros de sectorCollection con el estado PREVIA OK si la reserva está picked
*
* @param vSectorCollectionFk Identificador de vn.sectorCollection
*/
REPLACE saleTracking(saleFk, isChecked, workerFk, stateFk)
SELECT sgd.saleFk, TRUE, sc.userFk, s.id
FROM sectorCollection sc
JOIN sectorCollectionSaleGroup scsg ON scsg.sectorCollectionFk = sc.id
JOIN saleGroupDetail sgd ON sgd.saleGroupFk = scsg.saleGroupFk
JOIN state s ON s.code = 'OK PREVIOUS'
JOIN itemShelvingSale iss ON iss.saleFk = sgd.saleFk
WHERE sc.id = vSectorCollectionFk AND iss.isPicked;
END$$
DELIMITER ;

View File

@ -40,7 +40,7 @@ BEGIN
isTooLittle BOOL DEFAULT FALSE,
isVip BOOL DEFAULT FALSE,
PRIMARY KEY (ticketFk, saleFk)
) ENGINE = MEMORY;
); -- No memory
INSERT INTO tmp.sale_problems(ticketFk,
saleFk,

View File

@ -1,5 +1,7 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`ticket_getTax`(IN vTaxArea VARCHAR(25))
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`ticket_getTax`(
vTaxArea VARCHAR(25)
)
BEGIN
/**
* Calcula la base imponible, el IVA y el recargo de equivalencia para
@ -33,30 +35,39 @@ BEGIN
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketTax
(PRIMARY KEY (ticketFk, code, rate))
ENGINE = MEMORY
SELECT * FROM (
SELECT tmpTicket.ticketFk,
bp.pgcFk,
SUM(s.quantity * s.price * (100 - s.discount) / 100 ) taxableBase,
pgc.rate,
tc.code,
bp.priority
FROM tmp.ticket tmpTicket
JOIN sale s ON s.ticketFk = tmpTicket.ticketFk
JOIN item i ON i.id = s.itemFk
JOIN ticket t ON t.id = tmpTicket.ticketFk
JOIN supplier su ON su.id = t.companyFk
WITH sales AS (
SELECT s.ticketFk,
s.itemFk,
s.quantity * s.price * (100 - s.discount) / 100 total,
t.companyFk,
t.addressFk,
su.countryFk,
ata.areaFk,
itc.taxClassFk
FROM vn.sale s
JOIN tmp.ticket tmp ON tmp.ticketFk = s.ticketFk
JOIN vn.ticket t ON t.id = s.ticketFk
JOIN vn.supplier su ON su.id = t.companyFk
JOIN tmp.addressTaxArea ata ON ata.addressFk = t.addressFk
AND ata.companyFk = t.companyFk
JOIN itemTaxCountry itc ON itc.itemFk = i.id
JOIN vn.itemTaxCountry itc ON itc.itemFk = s.itemFk
AND itc.countryFk = su.countryFk
JOIN bookingPlanner bp ON bp.countryFk = su.countryFk
AND bp.taxAreaFk = ata.areaFk
AND bp.taxClassFk = itc.taxClassFk
JOIN pgc ON pgc.code = bp.pgcFk
JOIN taxClass tc ON tc.id = bp.taxClassFk
GROUP BY tmpTicket.ticketFk, pgc.code, pgc.rate
HAVING taxableBase
) t3
HAVING total
)
SELECT s.ticketFk,
bp.pgcFk,
SUM(s.total) taxableBase,
pgc.rate,
tc.code,
bp.priority
FROM sales s
JOIN vn.bookingPlanner bp ON bp.countryFk = s.countryFk
AND bp.taxAreaFk = s.areaFk
AND bp.taxClassFk = s.taxClassFk
JOIN vn.pgc ON pgc.code = bp.pgcFk
JOIN vn.taxClass tc ON tc.id = bp.taxClassFk
GROUP BY s.ticketFk, pgc.code, pgc.rate
HAVING taxableBase
ORDER BY priority;
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketServiceTax

View File

@ -21,6 +21,8 @@ BEGIN
SET businessFk = vNewBusinessFk
WHERE id = vSelf;
CALL queueMember_updateQueue(vNewBusinessFk);
IF vOldBusinessFk IS NULL THEN
CALL account.account_enable(vSelf);

View File

@ -3,10 +3,20 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` TRIGGER `vn`.`business_afterUpdate`
AFTER UPDATE ON `business`
FOR EACH ROW
BEGIN
DECLARE vIsActive BOOL;
DECLARE vExtension VARCHAR(10);
CALL worker_updateBusiness(NEW.workerFk);
IF NOT (OLD.workerFk <=> NEW.workerFk) THEN
CALL worker_updateBusiness(OLD.workerFk);
END IF;
IF NOT (OLD.departmentFk <=> NEW.departmentFk) THEN
SELECT COUNT(*) INTO vIsActive FROM worker WHERE businessFk = NEW.id;
IF vIsActive THEN
CALL queueMember_updateQueue(NEW.id);
END IF;
END IF;
END$$
DELIMITER ;

View File

@ -3,10 +3,10 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` TRIGGER `vn`.`itemShelvingSale_afterI
AFTER INSERT ON `itemShelvingSale`
FOR EACH ROW
BEGIN
UPDATE sale s
JOIN operator o ON o.workerFk = account.myUser_getId()
SET s.isPicked = IF(o.isOnReservationMode, s.isPicked, TRUE)
WHERE id = NEW.saleFk;
JOIN sector se ON se.id = o.sectorFk
SET s.isPicked = IF(IFNULL(se.isOnReservationMode, o.isOnReservationMode), s.isPicked, TRUE)
WHERE s.id = NEW.saleFk;
END$$
DELIMITER ;

View File

@ -9,5 +9,8 @@ BEGIN
SET NEW.userFk = account.myUser_getId();
END IF;
IF NEW.shelvingFk <> OLD.shelvingFk THEN
SET NEW.movingState = NULL;
END IF;
END$$
DELIMITER ;

View File

@ -22,7 +22,6 @@ AS SELECT `c`.`id` AS `id_cliente`,
`c`.`credit` AS `credito`,
`c`.`countryFk` AS `Id_Pais`,
`c`.`isActive` AS `activo`,
`c`.`gestdocFk` AS `gestdoc_id`,
`c`.`quality` AS `calidad`,
`c`.`payMethodFk` AS `pay_met_id`,
`c`.`created` AS `created`,

View File

@ -0,0 +1,3 @@
ALTER TABLE vn.clientObservation DROP COLUMN IF EXISTS observationTypeFk;
ALTER TABLE vn.clientObservation ADD COLUMN IF NOT EXISTS observationTypeFk TINYINT(3) UNSIGNED NULL;
ALTER TABLE vn.clientObservation ADD CONSTRAINT clientObservationTypeFk FOREIGN KEY IF NOT EXISTS (observationTypeFk) REFERENCES vn.observationType(id);

View File

@ -0,0 +1,3 @@
INSERT IGNORE INTO vn.observationType
SET description = 'Finance',
code = 'finance';

View File

@ -0,0 +1,3 @@
DELETE FROM salix.ACL
WHERE model = 'WorkerLog'
AND property = '*';

View File

@ -0,0 +1,3 @@
UPDATE vn.sale
SET originalQuantity = quantity
WHERE originalQuantity IS NULL

View File

@ -0,0 +1 @@
ALTER TABLE vn.sale MODIFY COLUMN originalQuantity decimal(10,2) DEFAULT 0.00 NOT NULL COMMENT 'Se utiliza para notificar a través de rocket los cambios de quantity';

View File

@ -0,0 +1,3 @@
ALTER TABLE vn.itemShelving DROP FOREIGN KEY itemShelving_fk2;
ALTER TABLE vn.itemShelving ADD CONSTRAINT itemShelving_fk2
FOREIGN KEY (shelvingFk) REFERENCES vn.shelving(code) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS isMoving BOOL DEFAULT FALSE NOT NULL COMMENT 'Indica que se ha marcado este registro para transferirlo a otro sector';

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE hedera.`order` ADD IF NOT EXISTS rowUpdated DATETIME NULL
COMMENT 'Timestamp for last updated record in orderRow table';

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE hedera.`order` ADD IF NOT EXISTS rowUpdated DATETIME NULL
COMMENT 'Timestamp for last updated record in orderRow table';

View File

@ -0,0 +1,2 @@
ALTER TABLE vn.sector ADD isOnReservationMode tinyint(1) DEFAULT FALSE NULL;

View File

@ -0,0 +1,6 @@
ALTER TABLE `vn`.`operator`
ADD COLUMN `machineFk` int(11) DEFAULT NULL,
ADD CONSTRAINT `operator_machine_FK` FOREIGN KEY (`machineFk`) REFERENCES `vn`.`machine` (`id`) ON DELETE SET NULL ON UPDATE CASCADE;
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES ('Machine','*','*','ALLOW','ROLE','productionBoss');

View File

@ -0,0 +1,4 @@
ALTER TABLE vn.docuwareConfig ADD IF NOT EXISTS username varchar(100) NULL;
ALTER TABLE vn.docuwareConfig ADD IF NOT EXISTS password varchar(100) NULL;
ALTER TABLE vn.docuwareConfig ADD IF NOT EXISTS token text NULL;
ALTER TABLE vn.docuwareConfig ADD IF NOT EXISTS expired int(11) NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`ticketConfig`
ADD COLUMN `closureDaysAgo` int(11) NOT NULL DEFAULT 2 COMMENT 'Number of days to look back for ticket closure',
ADD CONSTRAINT `closureDaysAgo_check` CHECK (`closureDaysAgo` > 0);

View File

@ -0,0 +1 @@
ALTER TABLE vn.client CHANGE gestdocFk gestdocFk__ int(11) DEFAULT NULL NULL COMMENT '@deprecated 2024-10-17';

View File

@ -1,42 +0,0 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Client Add notes path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
await page.accessToSearchResult('Bruce Banner');
await page.accessToSection('client.card.note.index');
});
afterAll(async() => {
await browser.close();
});
it(`should reach the notes index`, async() => {
await page.waitForState('client.card.note.index');
});
it(`should click on the add note button`, async() => {
await page.waitToClick(selectors.clientNotes.addNoteFloatButton);
await page.waitForState('client.card.note.create');
});
it(`should create a note`, async() => {
await page.waitForSelector(selectors.clientNotes.note);
await page.type(`${selectors.clientNotes.note} textarea`, 'Meeting with Black Widow 21st 9am');
await page.waitToClick(selectors.clientNotes.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the note was created', async() => {
const result = await page.waitToGetProperty(selectors.clientNotes.firstNoteText, 'innerText');
expect(result).toEqual('Meeting with Black Widow 21st 9am');
});
});

View File

@ -28,12 +28,12 @@ describe('Client defaulter path', () => {
const salesPersonName =
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
expect(clientName).toEqual('Bruce Banner');
expect(salesPersonName).toEqual('developer');
expect(clientName).toEqual('Ororo Munroe');
expect(salesPersonName).toEqual('salesperson');
});
it('should first observation not changed', async() => {
const expectedObservation = 'Meeting with Black Widow 21st 9am';
const expectedObservation = 'Madness, as you know, is like gravity, all it takes is a little push';
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(result).toContain(expectedObservation);
@ -62,13 +62,4 @@ describe('Client defaulter path', () => {
await page.write(selectors.clientDefaulter.observation, 'My new observation');
await page.waitToClick(selectors.clientDefaulter.saveButton);
});
it('should first observation changed', async() => {
const message = await page.waitForSnackbar();
await page.waitForSelector(selectors.clientDefaulter.firstObservation);
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(message.text).toContain('Observation saved!');
expect(result).toContain('My new observation');
});
});

View File

@ -1,46 +0,0 @@
import getBrowser from '../../helpers/puppeteer';
const $ = {
id: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(1) span',
alias: 'vn-order-summary vn-one:nth-child(1) > vn-label-value:nth-child(2) span',
consignee: 'vn-order-summary vn-one:nth-child(2) > vn-label-value:nth-child(6) span',
subtotal: 'vn-order-summary vn-one.taxes > p:nth-child(1)',
vat: 'vn-order-summary vn-one.taxes > p:nth-child(2)',
total: 'vn-order-summary vn-one.taxes > p:nth-child(3)',
sale: 'vn-order-summary vn-tbody > vn-tr',
};
describe('Order summary path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('16');
});
afterAll(async() => {
await browser.close();
});
it('should reach the order summary section and check data', async() => {
await page.waitForState('order.card.summary');
const id = await page.innerText($.id);
const alias = await page.innerText($.alias);
const consignee = await page.innerText($.consignee);
const subtotal = await page.innerText($.subtotal);
const vat = await page.innerText($.vat);
const total = await page.innerText($.total);
const sale = await page.countElement($.sale);
expect(id).toEqual('16');
expect(alias).toEqual('Many places');
expect(consignee).toEqual('address 26 - Gotham (Province one)');
expect(subtotal.length).toBeGreaterThan(1);
expect(vat.length).toBeGreaterThan(1);
expect(total.length).toBeGreaterThan(1);
expect(sale).toBeGreaterThan(0);
});
});

View File

@ -1,69 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
const $ = {
form: 'vn-order-basic-data form',
observation: 'vn-order-basic-data form [vn-name="note"]',
saveButton: `vn-order-basic-data form button[type=submit]`,
acceptButton: '.vn-confirm.shown button[response="accept"]'
};
describe('Order edit basic data path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('1');
await page.accessToSection('order.card.basicData');
});
afterAll(async() => {
await browser.close();
});
describe('when confirmed order', () => {
it('should not be able to change the client', async() => {
const message = await page.sendForm($.form, {
client: 'Tony Stark',
address: 'Tony Stark',
});
expect(message.text).toContain(`You can't make changes on the basic data`);
});
});
describe('when new order', () => {
it('should create an order and edit its basic data', async() => {
await page.waitToClick(selectors.globalItems.returnToModuleIndexButton);
await page.waitToClick($.acceptButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
await page.autocompleteSearch(selectors.createOrderView.client, 'Jessica Jones');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
await page.accessToSection('order.card.basicData');
const values = {
client: 'Tony Stark',
address: 'Tony Stark',
agencyMode: 'Other agency'
};
const message = await page.sendForm($.form, values);
await page.reloadSection('order.card.basicData');
const formValues = await page.fetchForm($.form, Object.keys(values));
expect(message.isSuccess).toBeTrue();
expect(formValues).toEqual(values);
});
});
});

View File

@ -1,48 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order lines', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
await page.accessToSearchResult('8');
await page.accessToSection('order.card.line');
});
afterAll(async() => {
await browser.close();
});
it('should check the order subtotal', async() => {
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('112.30');
});
it('should delete the first line in the order', async() => {
await page.waitToClick(selectors.orderLine.firstLineDeleteButton);
await page.waitToClick(selectors.orderLine.confirmButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the order subtotal has changed', async() => {
await page.waitForTextInElement(selectors.orderLine.orderSubtotal, '92.80');
const result = await page
.waitToGetProperty(selectors.orderLine.orderSubtotal, 'innerText');
expect(result).toContain('92.80');
});
it('should confirm the whole order and redirect to ticket index filtered by clientFk', async() => {
await page.waitToClick(selectors.orderLine.confirmOrder);
await page.expectURL('ticket/index');
await page.expectURL('clientFk');
});
});

View File

@ -1,97 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order catalog', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it('should open the create new order form', async() => {
await page.waitToClick(selectors.ordersIndex.createOrderButton);
await page.waitForState('order.create');
});
it('should create a new order', async() => {
await page.autocompleteSearch(selectors.createOrderView.client, 'Tony Stark');
await page.pickDate(selectors.createOrderView.landedDatePicker);
await page.autocompleteSearch(selectors.createOrderView.agency, 'Other agency');
await page.waitToClick(selectors.createOrderView.createButton);
await page.waitForState('order.card.catalog');
});
it('should add the realm and type filters and obtain results', async() => {
await page.waitToClick(selectors.orderCatalog.plantRealmButton);
await page.autocompleteSearch(selectors.orderCatalog.type, 'Anthurium');
await page.waitForNumberOfElements('section.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should perfom an "OR" search for the item tag colors silver and brown', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Color');
await page.autocompleteSearch(selectors.orderCatalog.firstTagAutocomplete, 'silver');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.autocompleteSearch(selectors.orderCatalog.secondTagAutocomplete, 'brown');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 4);
});
it('should perfom an "OR" search for the item tag tallos 2 and 9', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.addTagButton);
await page.write(selectors.orderCatalog.secondTagValue, '9');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 2);
});
it('should perform a general search for category', async() => {
await page.write(selectors.orderCatalog.itemTagValue, 'concussion');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements('section.product', 2);
});
it('should perfom an "AND" search for the item tag tallos 2', async() => {
await page.waitToClick(selectors.orderCatalog.openTagSearch);
await page.autocompleteSearch(selectors.orderCatalog.tag, 'Tallos');
await page.write(selectors.orderCatalog.firstTagValue, '2');
await page.waitToClick(selectors.orderCatalog.searchTagButton);
await page.waitForNumberOfElements('section.product', 1);
});
it('should remove the tag filters and have 4 results', async() => {
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.sixthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fifthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.fourthFilterRemoveButton);
await page.waitForContentLoaded();
await page.waitToClick(selectors.orderCatalog.thirdFilterRemoveButton);
await page.waitForNumberOfElements('.product', 4);
const result = await page.countElement('section.product');
expect(result).toEqual(4);
});
it('should search for an item by id', async() => {
await page.accessToSearchResult('2');
await page.waitForNumberOfElements('section.product', 1);
const result = await page.countElement('section.product');
expect(result).toEqual(1);
});
});

View File

@ -1,34 +0,0 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Order Index', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'order');
});
afterAll(async() => {
await browser.close();
});
it(`should check the second search result doesn't contain a total of 0€`, async() => {
await page.waitToClick(selectors.globalItems.searchButton);
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).not.toContain('0.00');
});
it('should search including empty orders', async() => {
await page.waitToClick(selectors.ordersIndex.openAdvancedSearch);
await page.waitToClick(selectors.ordersIndex.advancedSearchShowEmptyCheckbox);
await page.waitToClick(selectors.ordersIndex.advancedSearchButton);
await page.waitForTextInElement(selectors.ordersIndex.secondSearchResultTotal, '0.00');
const result = await page.waitToGetProperty(selectors.ordersIndex.secondSearchResultTotal, 'innerText');
expect(result).toContain('0.00');
});
});

View File

@ -66,10 +66,16 @@ export default class App {
]}
};
if (this.logger.$params.q)
newRoute = newRoute.concat(`?table=${this.logger.$params.q}`);
const hasId = !isNaN(parseInt(route.split('/')[1]));
if (this.logger.$params.q) {
let tableValue = this.logger.$params.q;
const q = JSON.parse(tableValue);
if (typeof q === 'number')
tableValue = JSON.stringify({id: tableValue});
newRoute = newRoute.concat(`?table=${tableValue}`);
} else if (!hasId && this.logger.$params.id && newRoute.indexOf(this.logger.$params.id) < 0)
newRoute = newRoute.concat(`${this.logger.$params.id}`);
return this.logger.$http.get('Urls/findOne', {filter})
.then(res => {

View File

@ -240,5 +240,6 @@
"There is already a tray with the same height": "There is already a tray with the same height",
"The height must be greater than 50cm": "The height must be greater than 50cm",
"The maximum height of the wagon is 200cm": "The maximum height of the wagon is 200cm",
"The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line"
"The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line",
"There are tickets for this area, delete them first": "There are tickets for this area, delete them first"
}

View File

@ -381,5 +381,7 @@
"The entry does not have stickers": "La entrada no tiene etiquetas",
"This buyer has already made a reservation for this date": "Este comprador ya ha hecho una reserva para esta fecha",
"No valid travel thermograph found": "No se encontró un termógrafo válido",
"The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea"
"The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea",
"type cannot be blank": "Se debe rellenar el tipo",
"There are tickets for this area, delete them first": "Hay tickets para esta sección, borralos primero"
}

View File

@ -25,7 +25,12 @@
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "user_id"
},
"queueMember": {
"type": "belongsTo",
"model": "QueueMember",
"foreignKey": "extension",
"primaryKey": "extension"
}
}
}
}

View File

@ -1,8 +1,11 @@
module.exports = function(Self) {
Self.validate('text', isEnabled, {message: 'Description cannot be blank'});
function isEnabled(err) {
Self.validate('text', function(err) {
if (!this.text) err();
}
}, {message: 'Description cannot be blank'});
Self.validate('observationTypeFk', function(err) {
if (!this.observationTypeFk) err();
}, {message: 'type cannot be blank'});
Self.observe('before save', function(ctx, next) {
ctx.instance.created = Date();

View File

@ -1,10 +1,10 @@
{
"name": "ClientObservation",
"name": "ClientObservation",
"description": "Client notes",
"base": "VnModel",
"mixins": {
"Loggable": true
},
"mixins": {
"Loggable": true
},
"options": {
"mysql": {
"table": "clientObservation"
@ -26,6 +26,10 @@
"created": {
"type": "date",
"description": "Creation date and time"
},
"observationTypeFk": {
"type": "number",
"description": "Type of observation"
}
},
"relations": {
@ -44,14 +48,18 @@
"include": {
"relation": "worker",
"scope": {
"fields": ["id"],
"fields": [
"id"
],
"include": {
"relation": "user",
"scope": {
"fields": ["nickname"]
"fields": [
"nickname"
]
}
}
}
}
}
}
}

View File

@ -54,7 +54,7 @@
<th field="clientFk">
<span translate>Client</span>
</th>
<th>
<th field="isWorker">
<span translate>Es trabajador</span>
</th>
<th field="salesPersonFk">

View File

@ -57,6 +57,11 @@ export default class Controller extends Section {
field: 'observation',
searchable: false
},
{
field: 'isWorker',
checkbox: true,
},
{
field: 'created',
datepicker: true
@ -73,9 +78,6 @@ export default class Controller extends Section {
set defaulters(value) {
if (!value || !value.length) return;
for (let defaulter of value)
defaulter.isWorker = defaulter.businessTypeFk === 'worker';
this._defaulters = value;
}
@ -164,6 +166,8 @@ export default class Controller extends Section {
exprBuilder(param, value) {
switch (param) {
case 'isWorker':
return {isWorker: value};
case 'creditInsurance':
case 'amount':
case 'clientFk':

View File

@ -1,31 +0,0 @@
<vn-crud-model
vn-id="model"
url="clientObservations"
filter="$ctrl.filter"
link="{clientFk: $ctrl.$params.id}"
data="notes"
auto-load="true">
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-md">
<vn-card class="vn-pa-md">
<div
ng-repeat="note in notes"
class="note vn-pa-sm border-solid border-radius vn-mb-md">
<vn-horizontal class="vn-mb-sm" style="color: #666">
<vn-one>{{::note.worker.user.nickname}}</vn-one>
<vn-auto>{{::note.created | date:'dd/MM/yyyy HH:mm'}}</vn-auto>
</vn-horizontal>
<vn-horizontal class="text">
{{::note.text}}
</vn-horizontal>
</div>
</vn-card>
</vn-data-viewer>
<a vn-tooltip="New note"
ui-sref="client.card.note.create({id: $ctrl.$params.id})"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>

View File

@ -5,9 +5,10 @@ import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
order: 'created DESC',
};
}
async $onInit() {
this.$state.go('home');
window.location.href = await this.vnApp.getUrl(`customer/${this.$params.id}/notes`);
}
}

View File

@ -39,7 +39,7 @@ describe('Entry filter()', () => {
const result = await models.Entry.filter(ctx, options);
expect(result.length).toEqual(11);
expect(result.length).toEqual(12);
await tx.rollback();
} catch (e) {
@ -152,7 +152,7 @@ describe('Entry filter()', () => {
const result = await models.Entry.filter(ctx, options);
expect(result.length).toEqual(10);
expect(result.length).toEqual(11);
await tx.rollback();
} catch (e) {

View File

@ -45,8 +45,8 @@ module.exports = Self => {
i.id itemFk,
i.name itemName,
ti.quantity,
(ac.conversionCoefficient * (ti.quantity / b.packing) * buy_getVolume(b.id))
/ (vc.trolleyM3 * 1000000) volume,
ROUND((ac.conversionCoefficient * (ti.quantity / b.packing) * buy_getVolume(b.id))
/ (vc.trolleyM3 * 1000000),1) volume,
b.packagingFk packagingFk,
b.packing
FROM tmp.item ti

View File

@ -0,0 +1,3 @@
<slot-descriptor>
<vn-entry-descriptor></vn-entry-descriptor>
</slot-descriptor>

View File

@ -0,0 +1,9 @@
import ngModule from '../module';
import DescriptorPopover from 'salix/components/descriptor-popover';
class Controller extends DescriptorPopover {}
ngModule.vnComponent('vnEntryDescriptorPopover', {
slotTemplate: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,65 @@
<vn-descriptor-content
module="entry"
description="$ctrl.entry.supplier.nickname"
summary="$ctrl.$.summary">
<slot-menu>
<vn-item
ng-click="$ctrl.showEntryReport()"
translate>
Show entry report
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value label="Agency "
value="{{$ctrl.entry.travel.agency.name}}">
</vn-label-value>
<vn-label-value label="Landed"
value="{{$ctrl.entry.travel.landed | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse Out"
value="{{$ctrl.entry.travel.warehouseOut.name}}">
</vn-label-value>
</div>
<div class="icons">
<vn-icon
vn-tooltip="Is inventory entry"
icon="icon-inventory"
ng-if="$ctrl.entry.isExcludedFromAvailable">
</vn-icon>
<vn-icon
vn-tooltip="Is virtual entry"
icon="icon-net"
ng-if="$ctrl.entry.isRaid">
</vn-icon>
</div>
<div class="quicklinks">
<div ng-transclude="btnOne">
<vn-quick-link
tooltip="Supplier card"
state="['supplier.index', {q: $ctrl.entry.supplier.id }]"
icon="icon-supplier">
</vn-quick-link>
</div>
<div ng-transclude="btnTwo">
<vn-quick-link
tooltip="All travels with current agency"
state="['travel.index', {q: $ctrl.travelFilter}]"
icon="local_airport">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
<vn-quick-link
tooltip="All entries with current supplier"
state="['entry.index', {q: $ctrl.entryFilter}]"
icon="icon-entry">
</vn-quick-link>
</div>
<div ng-transclude="btnThree">
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-popup vn-id="summary">
<vn-entry-summary entry="$ctrl.entry"></vn-entry-summary>
</vn-popup>

View File

@ -0,0 +1,99 @@
import ngModule from '../module';
import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor {
get entry() {
return this.entity;
}
set entry(value) {
this.entity = value;
}
get travelFilter() {
let travelFilter;
const entryTravel = this.entry && this.entry.travel;
if (entryTravel && entryTravel.agencyModeFk) {
travelFilter = this.entry && JSON.stringify({
agencyModeFk: entryTravel.agencyModeFk
});
}
return travelFilter;
}
get entryFilter() {
let entryTravel = this.entry && this.entry.travel;
if (!entryTravel || !entryTravel.landed) return null;
const date = new Date(entryTravel.landed);
date.setHours(0, 0, 0, 0);
const from = new Date(date.getTime());
from.setDate(from.getDate() - 10);
const to = new Date(date.getTime());
to.setDate(to.getDate() + 10);
return JSON.stringify({
supplierFk: this.entry.supplierFk,
from,
to
});
}
loadData() {
const filter = {
include: [
{
relation: 'travel',
scope: {
fields: ['id', 'landed', 'agencyModeFk', 'warehouseOutFk'],
include: [
{
relation: 'agency',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseOut',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseIn',
scope: {
fields: ['name']
}
}
]
}
},
{
relation: 'supplier',
scope: {
fields: ['id', 'nickname']
}
}
]
};
return this.getData(`Entries/${this.id}`, {filter})
.then(res => this.entity = res.data);
}
showEntryReport() {
this.vnReport.show(`Entries/${this.id}/entry-order-pdf`);
}
}
ngModule.vnComponent('vnEntryDescriptor', {
template: require('./index.html'),
controller: Controller,
bindings: {
entry: '<'
}
});

View File

@ -1,3 +1,6 @@
export * from './module';
import './main';
import './descriptor';
import './descriptor-popover';
import './summary';

View File

@ -8,6 +8,12 @@
"main": [
{"state": "entry.index", "icon": "icon-entry"},
{"state": "entry.latestBuys", "icon": "contact_support"}
],
"card": [
{"state": "entry.card.basicData", "icon": "settings"},
{"state": "entry.card.buy.index", "icon": "icon-lines"},
{"state": "entry.card.observation", "icon": "insert_drive_file"},
{"state": "entry.card.log", "icon": "history"}
]
},
"keybindings": [
@ -27,6 +33,90 @@
"component": "vn-entry-index",
"description": "Entries",
"acl": ["buyer", "administrative"]
},
{
"url": "/latest-buys?q",
"state": "entry.latestBuys",
"component": "vn-entry-latest-buys",
"description": "Latest buys",
"acl": ["buyer", "administrative"]
},
{
"url": "/create?supplierFk&travelFk&companyFk",
"state": "entry.create",
"component": "vn-entry-create",
"description": "New entry",
"acl": ["buyer", "administrative"]
},
{
"url": "/:id",
"state": "entry.card",
"abstract": true,
"component": "vn-entry-card"
},
{
"url": "/summary",
"state": "entry.card.summary",
"component": "vn-entry-summary",
"description": "Summary",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url": "/basic-data",
"state": "entry.card.basicData",
"component": "vn-entry-basic-data",
"description": "Basic data",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url": "/observation",
"state": "entry.card.observation",
"component": "vn-entry-observation",
"description": "Notes",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url" : "/log",
"state": "entry.card.log",
"component": "vn-entry-log",
"description": "Log",
"acl": ["buyer", "administrative"]
},
{
"url": "/buy",
"state": "entry.card.buy",
"abstract": true,
"component": "ui-view",
"acl": ["buyer"]
},
{
"url" : "/index",
"state": "entry.card.buy.index",
"component": "vn-entry-buy-index",
"description": "Buys",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer", "administrative"]
},
{
"url" : "/import",
"state": "entry.card.buy.import",
"component": "vn-entry-buy-import",
"description": "Import buys",
"params": {
"entry": "$ctrl.entry"
},
"acl": ["buyer"]
}
]
}

View File

@ -0,0 +1,195 @@
<vn-crud-model
vn-id="buysModel"
url="Entries/{{$ctrl.entry.id}}/getBuys"
limit="5"
data="buys"
auto-load="true">
</vn-crud-model>
<vn-card class="summary">
<h5>
<a ng-if="::$ctrl.entryData.id"
vn-tooltip="Go to the entry"
ui-sref="entry.card.summary({id: {{::$ctrl.entryData.id}}})"
name="goToSummary">
<vn-icon-button icon="launch"></vn-icon-button>
</a>
<span> #{{$ctrl.entryData.id}} - {{$ctrl.entryData.supplier.nickname}}</span>
</h5>
<vn-horizontal>
<vn-one>
<vn-label-value label="Commission"
value="{{$ctrl.entryData.commission}}">
</vn-label-value>
<vn-label-value label="Currency"
value="{{$ctrl.entryData.currency.name}}">
</vn-label-value>
<vn-label-value label="Company"
value="{{$ctrl.entryData.company.code}}">
</vn-label-value>
<vn-label-value label="Reference"
value="{{$ctrl.entryData.reference}}">
</vn-label-value>
<vn-label-value label="Invoice number"
value="{{$ctrl.entryData.invoiceNumber}}">
</vn-label-value>
</vn-one>
<vn-one>
<vn-label-value label="Reference">
<span
ng-click="travelDescriptor.show($event, $ctrl.entryData.travel.id)"
class="link">
{{$ctrl.entryData.travel.ref}}
</span>
</vn-label-value>
<vn-label-value label="Agency"
value="{{$ctrl.entryData.travel.agency.name}}">
</vn-label-value>
<vn-label-value label="Shipped"
value="{{$ctrl.entryData.travel.shipped | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse Out"
value="{{$ctrl.entryData.travel.warehouseOut.name}}">
</vn-label-value>
<vn-check
label="Delivered"
ng-model="$ctrl.entryData.travel.isDelivered"
disabled="true">
</vn-check>
<vn-label-value label="Landed"
value="{{$ctrl.entryData.travel.landed | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value label="Warehouse In"
value="{{$ctrl.entryData.travel.warehouseIn.name}}">
</vn-label-value>
<vn-check
label="Received"
ng-model="$ctrl.entryData.travel.isReceived"
disabled="true">
</vn-check>
</vn-one>
<vn-one>
<vn-vertical>
<vn-check
label="Ordered"
ng-model="$ctrl.entryData.isOrdered"
disabled="true">
</vn-check>
<vn-check
label="Confirmed"
ng-model="$ctrl.entryData.isConfirmed"
disabled="true">
</vn-check>
<vn-check
label="Booked"
ng-model="$ctrl.entryData.isBooked"
disabled="true">
</vn-check>
<vn-check
label="Raid"
ng-model="$ctrl.entryData.isRaid"
disabled="true">
</vn-check>
<vn-check
label="Inventory"
ng-model="$ctrl.entryData.isExcludedFromAvailable"
disabled="true">
</vn-check>
</vn-vertical>
</vn-one>
</vn-horizontal>
<vn-horizontal>
<vn-auto name="buys">
<h4 translate>Buys</h4>
<table class="vn-table">
<thead>
<tr>
<th translate center field="quantity">Quantity</th>
<th translate center field="sticker">Stickers</th>
<th translate center field="packagingFk">Package</th>
<th translate center field="weight">Weight</th>
<th translate center field="packing">Packing</th>
<th translate center field="grouping">Grouping</th>
<th translate center field="buyingValue">Buying value</th>
<th translate center field="price3">Import</th>
<th translate center expand field="price">PVP</th>
</tr>
</thead>
<tbody ng-repeat="line in buys">
<tr>
<td center title="{{::line.quantity}}">{{::line.quantity}}</td>
<td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td>
<td center title="{{::line.packagingFk | dashIfEmpty}}">{{::line.packagingFk | dashIfEmpty}}</td>
<td center title="{{::line.weight}}">{{::line.weight}}</td>
<td center>
<vn-chip class="transparent" translate-attr="line.groupingMode == 'packing' ? {title: 'Minimun amount'} : {title: 'Packing'}" ng-class="{'message': line.groupingMode == 'packing'}">
<span>{{::line.packing | dashIfEmpty}}</span>
</vn-chip>
</td>
<td center>
<vn-chip class="transparent" translate-attr="line.groupingMode == 'grouping' ? {title: 'Minimun amount'} : {title: 'Grouping'}" ng-class="{'message': line.groupingMode == 'grouping'}">
<span>{{::line.grouping | dashIfEmpty}}</span>
</vn-chip>
</vn-td>
<td center title="{{::line.buyingValue | currency: 'EUR':2}}">{{::line.buyingValue | currency: 'EUR':2}}</td>
<td center title="{{::line.quantity * line.buyingValue | currency: 'EUR':2}}">{{::line.quantity * line.buyingValue | currency: 'EUR':2}}</td>
<td center title="Grouping / Packing">{{::line.price2 | currency: 'EUR':2 | dashIfEmpty}} / {{::line.price3 | currency: 'EUR':2 | dashIfEmpty}}</td>
</tr>
<tr class="dark-row">
<td shrink>
<span
translate-attr="{title: 'Item type'}">
{{::line.item.itemType.code}}
</span>
</td>
<td shrink>
<span
ng-click="itemDescriptor.show($event, line.item.id)"
class="link">
{{::line.item.id}}
</span>
</td>
<td number shrink>
<span
translate-attr="{title: 'Item size'}">
{{::line.item.size}}
</span>
</td>
<td center>
<span
translate-attr="{title: 'Minimum price'}">
{{::line.item.minPrice | currency: 'EUR':2}}
</span>
</td>
<td vn-fetched-tags colspan="6">
<div>
<vn-one title="{{::line.item.concept}}">{{::line.item.name}}</vn-one>
<vn-one ng-if="::line.item.subName">
<h3 title="{{::line.item.subName}}">{{::line.item.subName}}</h3>
</vn-one>
</div>
<vn-fetched-tags
max-length="6"
item="::line.item"
tabindex="-1">
</vn-fetched-tags>
</td>
</tr>
<tr class="empty-row">
<td colspan="10"></td>
</tr>
</tbody>
</table>
<vn-pagination
model="buysModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto>
</vn-horizontal>
</vn-card>
<vn-item-descriptor-popover
vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover>
<vn-travel-descriptor-popover
vn-id="travelDescriptor">
</vn-travel-descriptor-popover>

View File

@ -0,0 +1,33 @@
import ngModule from '../module';
import './style.scss';
import Summary from 'salix/components/summary';
class Controller extends Summary {
get entry() {
if (!this._entry)
return this.$params;
return this._entry;
}
set entry(value) {
this._entry = value;
if (value && value.id)
this.getEntryData();
}
getEntryData() {
return this.$http.get(`Entries/${this.entry.id}/getEntry`).then(response => {
this.entryData = response.data;
});
}
}
ngModule.vnComponent('vnEntrySummary', {
template: require('./index.html'),
controller: Controller,
bindings: {
entry: '<'
}
});

View File

@ -0,0 +1,30 @@
@import "variables";
vn-entry-summary .summary {
max-width: $width-lg;
.dark-row {
background-color: lighten($color-marginal, 10%);
}
tbody tr:nth-child(1) {
border-top: $border-thin;
}
tbody tr:nth-child(1),
tbody tr:nth-child(2) {
border-left: $border-thin;
border-right: $border-thin
}
tbody tr:nth-child(3) {
height: inherit
}
tr {
margin-bottom: 10px;
}
}
$color-font-link-medium: lighten($color-font-link, 20%)

View File

@ -48,13 +48,13 @@ module.exports = Self => {
let stmt;
stmts.push(new ParameterizedSQL(
`CREATE OR REPLACE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
(INDEX (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticket t
FROM ticket
WHERE shipped BETWEEN ? AND util.dayEnd(?)
AND refFk IS NULL`, [args.from, args.to]));
stmts.push(`CALL vn.ticket_getTax(NULL)`);
stmts.push(`CALL ticket_getTax(NULL)`);
stmts.push(new ParameterizedSQL(
`CREATE OR REPLACE TEMPORARY TABLE tmp.filter
ENGINE = MEMORY
@ -71,12 +71,12 @@ module.exports = Self => {
c.isTaxDataChecked,
w.id comercialId,
u.name workerName
FROM vn.ticket t
JOIN vn.company co ON co.id = t.companyFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.client c ON c.id = t.clientFk
JOIN vn.country cou ON cou.id = c.countryFk
LEFT JOIN vn.worker w ON w.id = c.salesPersonFk
FROM ticket t
JOIN company co ON co.id = t.companyFk
JOIN sale s ON s.ticketFk = t.id
JOIN client c ON c.id = t.clientFk
JOIN country cou ON cou.id = c.countryFk
LEFT JOIN worker w ON w.id = c.salesPersonFk
JOIN account.user u ON u.id = w.id
LEFT JOIN (
SELECT ticketFk, taxableBase

View File

@ -34,6 +34,11 @@ module.exports = Self => {
try {
const promises = [];
for (let itemShelvingId of itemShelvingIds) {
const itemShelvingSaleToDelete = models.ItemShelvingSale.destroyAll({
itemShelvingFk: itemShelvingId
}, myOptions);
promises.push(itemShelvingSaleToDelete);
const itemShelvingToDelete = models.ItemShelving.destroyById(itemShelvingId, myOptions);
promises.push(itemShelvingToDelete);
}

View File

@ -10,7 +10,7 @@ describe('ItemShelving deleteItemShelvings()', () => {
const itemShelvingIds = [1, 2];
const result = await models.ItemShelving.deleteItemShelvings(itemShelvingIds, options);
expect(result.length).toEqual(2);
expect(result.length).toEqual(4);
await tx.rollback();
} catch (e) {

View File

@ -1,6 +1,6 @@
const {models} = require('vn-loopback/server/server');
describe('item lastEntriesFilter()', () => {
it('should return one entry for the given item', async() => {
it('should return two entry for the given item', async() => {
const minDate = Date.vnNew();
minDate.setHours(0, 0, 0, 0);
const maxDate = Date.vnNew();
@ -13,7 +13,7 @@ describe('item lastEntriesFilter()', () => {
const filter = {where: {itemFk: 1, landed: {between: [minDate, maxDate]}}};
const result = await models.Item.lastEntriesFilter(filter, options);
expect(result.length).toEqual(1);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
@ -22,7 +22,7 @@ describe('item lastEntriesFilter()', () => {
}
});
it('should return five entries for the given item', async() => {
it('should return six entries for the given item', async() => {
const minDate = Date.vnNew();
minDate.setHours(0, 0, 0, 0);
minDate.setMonth(minDate.getMonth() - 2, 1);
@ -37,7 +37,7 @@ describe('item lastEntriesFilter()', () => {
const filter = {where: {itemFk: 1, landed: {between: [minDate, maxDate]}}};
const result = await models.Item.lastEntriesFilter(filter, options);
expect(result.length).toEqual(5);
expect(result.length).toEqual(6);
await tx.rollback();
} catch (e) {

View File

@ -3,7 +3,7 @@
"name": "Items",
"icon": "icon-item",
"validations" : true,
"dependencies": ["worker", "client", "ticket"],
"dependencies": ["worker", "client", "ticket", "entry"],
"menus": {
"main": [
{"state": "item.index", "icon": "icon-item"},

View File

@ -1,8 +1,3 @@
export * from './module';
import './main';
import './index/';
import './index/tickets';
import './index/clients';
import './index/orders';
import './index/search-panel';

View File

@ -1,110 +0,0 @@
<vn-crud-model
vn-id="model"
url="SalesMonitors/clientsFilter"
limit="6"
filter="$ctrl.filter"
order="dated DESC, hour DESC"
auto-load="true">
</vn-crud-model>
<vn-horizontal class="header">
<vn-one translate>
Clients on website
</vn-one>
<vn-none>
<vn-icon class="arrow"
icon="keyboard_arrow_up"
vn-tooltip="Minimize/Maximize"
ng-click="$ctrl.main.toggle()">
</vn-icon>
</vn-none>
</vn-horizontal>
<vn-card vn-id="card">
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"
disabled-table-filter="true"
disabled-table-order="true"
class="scrollable sm">
<slot-actions>
<vn-horizontal>
<vn-date-picker
class="vn-pa-xs"
label="From"
ng-model="$ctrl.dateFrom"
on-change="$ctrl.addFilterDate()">
</vn-date-picker>
<vn-date-picker
class="vn-pa-xs"
label="To"
ng-model="$ctrl.dateTo"
on-change="$ctrl.addFilterDate()">
</vn-date-picker>
</vn-horizontal>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th field="dated">
<span translate>Date</span>
</th>
<th field="hour">
<span translate>Hour</span>
</th>
<th field="salesPersonFk" class="expendable">
<span translate>Salesperson</span>
</th>
<th field="clientFk">
<span translate>Client</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="visit in model.data track by visit.id">
<td shrink-date>
<span class="chip">
{{::visit.dated | date:'dd/MM/yy'}}
</span>
</td>
<td shrink-date>
<span class="chip">
{{::visit.hour | date: 'HH:mm'}}
</span>
</td>
<td class="shrink expendable">
<span
title="{{::visit.salesPerson}}"
vn-click-stop="workerDescriptor.show($event, visit.salesPersonFk)"
class="link">
{{::visit.salesPerson | dashIfEmpty}}
</span>
</td>
<td>
<span
title="{{::visit.clientName}}"
vn-click-stop="clientDescriptor.show($event, visit.clientFk)"
class="link">
{{::visit.clientName}}
</span>
</td>
</tr>
</tbody>
</table>
</slot-table>
<slot-pagination>
<vn-pagination
model="model"
class="vn-pt-xs"
scroll-selector="vn-monitor-sales-clients smart-table"
scroll-offset="100">
</vn-pagination>
</slot-pagination>
</smart-table>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>

View File

@ -1,96 +0,0 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
const date = Date.vnNew();
this.dateFrom = date;
this.dateTo = date;
this.filter = {
where: {
'v.stamp': {
between: this.dateRange()
}
}
};
this.smartTableOptions = {
activeButtons: {
search: true
},
columns: [
{
field: 'clientFk',
autocomplete: {
url: 'Clients',
showField: 'name',
valueField: 'id'
}
},
{
field: 'salesPersonFk',
autocomplete: {
url: 'Workers/activeWithInheritedRole',
where: `{role: 'salesPerson'}`,
searchFunction: '{firstName: $search}',
showField: 'nickname',
valueField: 'id',
}
},
{
field: 'dated',
searchable: false
},
{
field: 'hour',
searchable: false
}
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'clientFk':
return {[`c.id`]: value};
case 'salesPersonFk':
return {[`c.${param}`]: value};
}
}
dateRange() {
let from = this.dateFrom;
let to = this.dateTo;
if (!from)
from = Date.vnNew();
if (!to)
to = Date.vnNew();
const minHour = new Date(from);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(to);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
addFilterDate() {
this.$.model.filter = {
where: {
'v.stamp': {
between: this.dateRange()
}
}
};
this.$.model.refresh();
}
}
ngModule.vnComponent('vnMonitorSalesClients', {
template: require('./index.html'),
controller: Controller,
require: {
main: '^vnMonitorIndex'
}
});

View File

@ -1,5 +0,0 @@
<vn-horizontal ng-class="{'hidden': $ctrl.isTopPanelHidden}">
<vn-monitor-sales-clients class="vn-mb-sm"></vn-monitor-sales-clients>
<vn-monitor-sales-orders></vn-monitor-sales-orders>
</vn-horizontal>
<vn-monitor-sales-tickets></vn-monitor-sales-tickets>

View File

@ -1,34 +0,0 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
const isTopPanelHidden = localStorage.getItem('ticketTopPanelHidden');
if (isTopPanelHidden === 'true')
this.isTopPanelHidden = true;
}
toggle() {
const monitor = this.element.querySelector('vn-horizontal');
const isHidden = monitor.classList.contains('hidden');
if (!isHidden) {
monitor.classList.add('hidden');
localStorage.setItem('ticketTopPanelHidden', true);
} else {
monitor.classList.remove('hidden');
localStorage.setItem('ticketTopPanelHidden', false);
}
}
}
ngModule.vnComponent('vnMonitorIndex', {
template: require('./index.html'),
controller: Controller,
require: {
main: '^vnMonitorIndex'
}
});

View File

@ -1,38 +0,0 @@
import './index.js';
describe('Component vnMonitorIndex', () => {
let controller;
let $element;
beforeEach(ngModule('monitor'));
beforeEach(inject(($compile, $rootScope) => {
$element = $compile('<vn-monitor-index></vn-monitor-index>')($rootScope);
controller = $element.controller('vnMonitorIndex');
}));
describe('toggle()', () => {
it('should add the hidden class to the horizontal contaning the panel to hide', () => {
controller.toggle();
const targetElement = $element[0].querySelector('vn-horizontal');
const firstClass = targetElement.classList[0];
const isTopPanelHidden = localStorage.getItem('ticketTopPanelHidden');
expect(firstClass).toEqual('hidden');
expect(isTopPanelHidden).toEqual('true');
});
it('should remove the hidden class to the horizontal contaning the panel to hide', () => {
const targetElement = $element[0].querySelector('vn-horizontal');
targetElement.classList.add('hidden');
controller.toggle();
const firstClass = targetElement.classList[0];
const isTopPanelHidden = localStorage.getItem('ticketTopPanelHidden');
expect(firstClass).toBeUndefined();
expect(isTopPanelHidden).toEqual('false');
});
});
});

View File

@ -1,16 +0,0 @@
Tickets monitor: Monitor de tickets
Clients on website: Clientes activos en la web
Recent order actions: Acciones recientes en pedidos
Search tickets: Buscar tickets
Delete selected elements: Eliminar los elementos seleccionados
All the selected elements will be deleted. Are you sure you want to continue?: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar?
Component lack: Faltan componentes
Ticket too little: Ticket demasiado pequeño
Minimize/Maximize: Minimizar/Maximizar
Problems: Problemas
Theoretical: Teórica
Practical: Práctica
Preparation: Preparación
Auto-refresh: Auto-refresco
Toggle auto-refresh every 2 minutes: Conmuta el refresco automático cada 2 minutos
Is fragile: Es frágil

View File

@ -1,141 +0,0 @@
<vn-crud-model auto-load="true"
vn-id="model"
url="SalesMonitors/ordersFilter"
limit="6"
order="date_make DESC">
</vn-crud-model>
<vn-horizontal class="header">
<vn-one translate>
Recent order actions
</vn-one>
<vn-none>
<vn-icon
icon="delete"
vn-tooltip="Delete"
ng-if="$ctrl.totalChecked > 0"
ng-click="delete.show()">
</vn-icon>
<vn-icon class="arrow"
icon="keyboard_arrow_up"
vn-tooltip="Minimize/Maximize"
ng-click="$ctrl.main.toggle()">
</vn-icon>
<vn-icon
icon="refresh"
vn-tooltip="Refresh"
ng-click="model.refresh()">
</vn-icon>
</vn-none>
</vn-horizontal>
<vn-card>
<vn-table model="model" class="scrollable">
<vn-thead>
<vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="date_send" shrink-datetime>Date</vn-th>
<vn-th field="clientFk">Client</vn-th>
<vn-th field="salesPersonFk" shrink>SalesPerson</vn-th>
</vn-tr>
</vn-thead>
<a ng-repeat="order in model.data track by order.id"
class="clickable vn-tbody"
ui-sref="order.card.summary({id: {{::order.id}}})" target="_blank">
<vn-tr>
<vn-td>
<vn-check
ng-model="order.checked"
vn-click-stop>
</vn-check>
</vn-td>
<vn-td>
<span class="chip {{::$ctrl.chipColor(order.date_send)}}">
{{::order.date_send | date: 'dd/MM/yyyy'}}
</span>
</vn-td>
<vn-td>
<span
title="{{::order.clientName}}"
vn-click-stop="clientDescriptor.show($event, order.clientFk)"
class="link">
{{::order.clientName}}
</span>
</vn-td>
<vn-td>
<span
title="{{::order.salesPerson}}"
vn-click-stop="workerDescriptor.show($event, order.salesPersonFk)"
class="link">
{{::order.salesPerson | dashIfEmpty}}
</span>
</vn-td>
</vn-tr>
<vn-tr>
<vn-td></vn-td>
<vn-td>
<span>
{{::order.date_make | date: 'dd/MM/yyyy HH:mm'}}
</span>
</vn-td>
<vn-td>
<span title="{{::order.agencyName}}">
{{::order.agencyName | dashIfEmpty}}
</span>
</vn-td>
<vn-td>
{{::order.import | currency: 'EUR':2}}
</vn-td>
</vn-tr>
</a>
</vn-table>
<vn-pagination
model="model"
class="vn-pt-xs"
scroll-selector="vn-monitor-sales-orders vn-table"
scroll-offset="100">
</vn-pagination>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-contextmenu vn-id="contextmenu" targets="['vn-monitor-sales-orders vn-table']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>
<vn-confirm
vn-id="delete"
on-accept="$ctrl.onDelete()"
question="All the selected elements will be deleted. Are you sure you want to continue?"
message="Delete selected elements">
</vn-confirm>

View File

@ -1,74 +0,0 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
get checked() {
const rows = this.$.model.data || [];
const checkedRows = [];
for (let row of rows) {
if (row.checked)
checkedRows.push(row.id);
}
return checkedRows;
}
get totalChecked() {
return this.checked.length;
}
onDelete() {
const params = {deletes: this.checked};
const query = `SalesMonitors/deleteOrders`;
this.$http.post(query, params).then(
() => this.$.model.refresh());
}
chipColor(date) {
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
const orderLanded = new Date(date);
orderLanded.setHours(0, 0, 0, 0);
const difference = today - orderLanded;
if (difference == 0)
return 'warning';
if (difference < 0)
return 'success';
if (difference > 0)
return 'alert';
}
exprBuilder(param, value) {
switch (param) {
case 'date_send':
return {[`o.date_send`]: {
between: this.dateRange(value)}
};
case 'clientFk':
return {[`c.id`]: value};
case 'salesPersonFk':
return {[`c.${param}`]: value};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
}
ngModule.vnComponent('vnMonitorSalesOrders', {
template: require('./index.html'),
controller: Controller,
require: {
main: '^vnMonitorIndex'
}
});

View File

@ -1,50 +0,0 @@
import './index.js';
import crudModel from 'core/mocks/crud-model';
describe('Component vnMonitorSalesOrders', () => {
let controller;
let $httpBackend;
beforeEach(ngModule('monitor'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-monitor-sales-orders></vn-monitor-sales-orders>');
controller = $componentController('vnMonitorSalesOrders', {$element});
controller.$.model = crudModel;
controller.$.model.data = [
{id: 1, name: 'My item 1'},
{id: 2, name: 'My item 2'},
{id: 3, name: 'My item 3'}
];
}));
describe('checked() getter', () => {
it('should return a the selected rows', () => {
const modelData = controller.$.model.data;
modelData[0].checked = true;
modelData[1].checked = true;
const result = controller.checked;
expect(result).toEqual(expect.arrayContaining([1, 2]));
});
});
describe('onDelete()', () => {
it('should make a query and then call to the model refresh() method', () => {
jest.spyOn(controller.$.model, 'refresh');
const modelData = controller.$.model.data;
modelData[0].checked = true;
modelData[1].checked = true;
const expectedParams = {deletes: [1, 2]};
$httpBackend.expect('POST', 'SalesMonitors/deleteOrders', expectedParams).respond(200);
controller.onDelete();
$httpBackend.flush();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
});

View File

@ -1,19 +0,0 @@
@import "variables";
vn-monitor-sales-orders {
vn-table.scrollable {
max-height: 350px;
overflow-x: hidden
}
vn-table a.vn-tbody {
& > vn-tr:nth-child(2) {
color: gray;
& > vn-td {
border-bottom: $border;
font-size: 13px;
}
}
}
}

View File

@ -1,120 +0,0 @@
<div class="search-panel">
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search ticket by id or alias"
vn-focus>
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Client id"
ng-model="filter.clientFk">
</vn-textfield>
<vn-textfield
vn-one
label="Order id"
ng-model="filter.orderFk">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</vn-input-number>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Nickname"
ng-model="filter.nickname">
</vn-textfield>
<vn-worker-autocomplete
vn-one
ng-model="filter.salesPersonFk"
departments="['VT']"
label="Sales person">
</vn-worker-autocomplete>
<vn-textfield
vn-one
label="Invoice"
ng-model="filter.refFk">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes/isActive">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="State"
ng-model="filter.stateFk"
url="States">
</vn-autocomplete>
<vn-autocomplete vn-one
data="$ctrl.groupedStates"
label="Grouped States"
value-field="id"
show-field="name"
ng-model="filter.alertLevel">
<tpl-item>
{{name}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="filter.warehouseFk"
data="warehouses">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Province"
ng-model="filter.provinceFk"
url="Provinces">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-check
vn-one
label="My team"
ng-model="filter.myTeam"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="With problems"
ng-model="filter.problems"
triple-state="true">
</vn-check>
<vn-check
vn-one
label="Pending"
ng-model="filter.pending"
triple-state="true">
</vn-check>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>

View File

@ -1,59 +0,0 @@
import ngModule from '../../module';
import SearchPanel from 'core/components/searchbar/search-panel';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
this.getGroupedStates();
}
getGroupedStates() {
let groupedStates = [];
this.$http.get('AlertLevels').then(res => {
for (let state of res.data) {
groupedStates.push({
id: state.id,
code: state.code,
name: this.$t(state.code)
});
}
this.groupedStates = groupedStates;
});
}
get from() {
return this._from;
}
set from(value) {
this._from = value;
this.filter.scopeDays = null;
}
get to() {
return this._to;
}
set to(value) {
this._to = value;
this.filter.scopeDays = null;
}
get scopeDays() {
return this._scopeDays;
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.from = null;
this.filter.to = null;
}
}
ngModule.vnComponent('vnMonitorSalesSearchPanel', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,71 +0,0 @@
import './index';
describe('Monitor Component vnMonitorSalesSearchPanel', () => {
let $httpBackend;
let controller;
beforeEach(ngModule('monitor'));
beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
controller = $componentController('vnMonitorSalesSearchPanel', {$element: null});
controller.$t = () => {};
controller.filter = {};
}));
describe('getGroupedStates()', () => {
it('should set an array of groupedStates with the adition of a name translation', () => {
jest.spyOn(controller, '$t').mockReturnValue('miCodigo');
const data = [
{
id: 9999,
code: 'myCode'
}
];
$httpBackend.whenGET('AlertLevels').respond(data);
controller.getGroupedStates();
$httpBackend.flush();
expect(controller.groupedStates).toEqual([{
id: 9999,
code: 'myCode',
name: 'miCodigo'
}]);
});
});
describe('from() setter', () => {
it('should clear the scope days when setting the from property', () => {
controller.filter.scopeDays = 1;
controller.from = Date.vnNew();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.from).toBeDefined();
});
});
describe('to() setter', () => {
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.to = Date.vnNew();
expect(controller.filter.scopeDays).toBeNull();
expect(controller.to).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.from = Date.vnNew();
controller.filter.to = Date.vnNew();
controller.scopeDays = 1;
expect(controller.filter.from).toBeNull();
expect(controller.filter.to).toBeNull();
expect(controller.scopeDays).toBeDefined();
});
});
});

View File

@ -1,85 +0,0 @@
@import "variables";
@import "effects";
vn-monitor-index {
.header {
padding: 12px 0 5px 0;
color: gray;
font-size: 1.2rem;
border-bottom: $border;
margin-bottom: 10px;
& > vn-none > vn-icon {
@extend %clickable-light;
color: $color-button;
font-size: 1.4rem;
}
vn-none > .arrow {
transition: transform 200ms;
}
}
vn-monitor-sales-clients {
vn-card {
margin-right: 15px;
}
.header {
padding-right: 15px;
& > vn-none > .arrow {
display: none
}
}
}
vn-table.scrollable {
height: 300px
}
vn-horizontal {
flex-wrap: wrap
}
.hidden {
vn-card {
display: none
}
.header > vn-none > .arrow {
transform: rotate(180deg);
}
}
}
@media (max-width:1150px) {
vn-monitor-index {
& > vn-horizontal {
flex-direction: column;
vn-monitor-sales-clients,
vn-monitor-sales-orders {
width: 100%
}
vn-monitor-sales-clients {
margin-bottom: 15px
}
}
vn-monitor-sales-clients {
vn-card {
margin-right: 0
}
.header {
padding-right: 0;
& > vn-none > .arrow {
display: inline-block
}
}
}
}
}

View File

@ -1,270 +0,0 @@
<vn-crud-model
vn-id="model"
url="SalesMonitors/salesFilter"
auto-load="false"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-monitor-sales-search-panel"
placeholder="Search tickets"
info="Search ticket by id or alias"
model="model"
fetch-params="$ctrl.fetchParams($params)"
suggested-filter="$ctrl.filterParams"
auto-state="false">
</vn-searchbar>
</vn-portal>
<vn-horizontal class="header">
<vn-one translate>
Tickets monitor
</vn-one>
</vn-horizontal>
<vn-card>
<smart-table
model="model"
view-config-id="ticketsMonitor"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<vn-check
label="Auto-refresh"
vn-tooltip="Toggle auto-refresh every 2 minutes"
on-change="$ctrl.autoRefresh(value)">
</vn-check>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th field="totalProblems" menu-enabled="false">
<span translate>Problems</span>
</th>
<th field="id">
<span translate>Identifier</span>
</th>
<th field="nickname">
<span translate>Client</span>
</th>
<th field="salesPersonFk">
<span translate>Salesperson</span>
</th>
<th field="shippedDate" shrink-date filter-enabled="false">
<span translate>Date</span>
</th>
<th field="theoreticalHour" filter-enabled="false">
<span translate>Theoretical</span>
</th>
<th field="practicalHour" filter-enabled="false">
<span translate>Practical</span>
</th>
<th field="preparationHour" filter-enabled="false">
<span translate>Preparation</span>
</th>
<th field="provinceFk">
<span translate>Province</span>
</th>
<th field="stateFk">
<span translate>State</span>
</th>
<th field="isFragile" number>
<span translate>Fragile</span>
</th>
<th field="zoneFk">
<span translate>Zone</span>
</th>
<th field="totalWithVat" shrink>
<span translate>Total</span>
</th>
<th shrink></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ticket in model.data track by ticket.id"
vn-anchor="{
state: 'ticket.card.summary',
params: {id: ticket.id},
target: '_blank'
}">
<td>
<vn-icon
ng-show="ticket.isTaxDataChecked === 0"
translate-attr="{title: 'No verified data'}"
class="bright"
icon="icon-no036">
</vn-icon>
<vn-icon
ng-show="ticket.hasTicketRequest"
translate-attr="{title: 'Purchase request'}"
class="bright"
icon="icon-buyrequest">
</vn-icon>
<vn-icon
ng-show="ticket.itemShortage"
translate-attr="{title: 'Not visible'}"
class="bright"
icon="icon-unavailable">
</vn-icon>
<vn-icon
ng-show="ticket.isFreezed"
translate-attr="{title: 'Client frozen'}"
class="bright"
icon="icon-frozen">
</vn-icon>
<vn-icon
ng-show="ticket.risk"
ng-class="{'highRisk': ticket.hasHighRisk}"
title="{{$ctrl.$t('Risk')}}: {{ticket.risk}}"
class="bright"
icon="icon-risk">
</vn-icon>
<vn-icon
ng-show="ticket.hasComponentLack"
translate-attr="{title: 'Component lack'}"
class="bright"
icon="icon-components">
</vn-icon>
<vn-icon
ng-show="ticket.isTooLittle"
translate-attr="{title: 'Ticket too little'}"
class="bright"
icon="icon-isTooLittle">
</vn-icon>
</td>
<td>
<span
vn-click-stop="ticketDescriptor.show($event, ticket.id)"
class="link">
{{ticket.id}}
</span>
</td>
<td name="nickname">
<span
title="{{ticket.nickname}}"
vn-click-stop="clientDescriptor.show($event, ticket.clientFk)"
class="link">
{{ticket.nickname}}
</span>
</td>
<td>
<span
title="{{ticket.userName}}"
vn-click-stop="workerDescriptor.show($event, ticket.salesPersonFk)"
class="link">
{{ticket.userName | dashIfEmpty}}
</span>
</td>
<td>
<span class="chip {{$ctrl.compareDate(ticket.shippedDate)}}">
{{ticket.shippedDate | date: 'dd/MM/yyyy'}}
</span>
</td>
<td>{{ticket.zoneLanding | date: 'HH:mm'}}</td>
<td>{{ticket.practicalHour | date: 'HH:mm'}}</td>
<td>{{ticket.shipped | date: 'HH:mm'}}</td>
<td>{{ticket.province}}</td>
<td>
<span
ng-show="ticket.refFk"
title="{{ticket.refFk}}"
vn-click-stop="invoiceOutDescriptor.show($event, ticket.invoiceOutId)"
class="link">
{{ticket.refFk}}
</span>
<span
ng-show="!ticket.refFk"
class="chip {{ticket.classColor}}">
{{ticket.state}}
</span>
</td>
<td number>
<vn-icon
ng-show="ticket.isFragile"
translate-attr="{title: 'Is fragile'}"
class="bright"
icon="local_bar">
</vn-icon>
</td>
<td name="zone">
<span
title="{{ticket.zoneName}}"
vn-click-stop="zoneDescriptor.show($event, ticket.zoneFk)"
class="link">
{{ticket.zoneName | dashIfEmpty}}
</span>
</td>
<td number>
<span class="chip {{$ctrl.totalPriceColor(ticket)}}">
{{(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}}
</span>
</td>
<td actions>
<vn-icon-button
vn-anchor="{
state: 'ticket.card.sale',
params: {id: ticket.id},
target: '_blank'
}"
vn-tooltip="Go to lines"
icon="icon-lines">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(ticket)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-ticket-descriptor-popover
vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<vn-zone-descriptor-popover
vn-id="zoneDescriptor">
</vn-zone-descriptor-popover>
<vn-popup vn-id="summary">
<vn-ticket-summary
ticket="$ctrl.selectedTicket"
model="model">
</vn-ticket-summary>
</vn-popup>
<vn-contextmenu vn-id="contextmenu" targets="['vn-monitor-sales-tickets smart-table']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -1,178 +0,0 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filterParams = this.fetchParams();
this.smartTableOptions = {
activeButtons: {
search: true,
shownColumns: true,
},
columns: [
{
field: 'totalProblems',
searchable: false
},
{
field: 'salesPersonFk',
autocomplete: {
url: 'Workers/activeWithInheritedRole',
where: `{role: 'salesPerson'}`,
searchFunction: '{firstName: $search}',
showField: 'nickname',
valueField: 'id',
}
},
{
field: 'provinceFk',
autocomplete: {
url: 'Provinces',
}
},
{
field: 'stateFk',
autocomplete: {
url: 'States',
}
},
{
field: 'zoneFk',
autocomplete: {
url: 'Zones',
}
},
{
field: 'warehouseFk',
autocomplete: {
url: 'Warehouses',
}
},
{
field: 'shippedDate',
datepicker: true
},
{
field: 'theoreticalHour',
searchable: false
},
{
field: 'preparationHour',
searchable: false
}
]
};
}
$onInit() {
if (!this.$params.q) {
this.$.$applyAsync(
() => this.$.model.applyFilter(null, this.filterParams));
}
}
fetchParams($params = {}) {
const excludedParams = [
'search',
'clientFk',
'orderFk',
'refFk',
'scopeDays'
];
const hasExcludedParams = excludedParams.some(param => {
return $params && $params[param] != undefined;
});
const hasParams = Object.entries($params).length;
if (!hasParams || !hasExcludedParams)
$params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') {
const from = Date.vnNew();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + $params.scopeDays);
to.setHours(23, 59, 59, 999);
Object.assign($params, {from, to});
}
return $params;
}
compareDate(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0)
return 'warning';
if (comparation < 0)
return 'success';
}
totalPriceColor(ticket) {
const total = parseInt(ticket.totalWithVat);
if (total > 0 && total < 50)
return 'warning';
}
exprBuilder(param, value) {
switch (param) {
case 'stateFk':
return {'ts.stateFk': value};
case 'salesPersonFk':
return {'c.salesPersonFk': value};
case 'provinceFk':
return {'a.provinceFk': value};
case 'theoreticalHour':
return {'z.hour': value};
case 'practicalHour':
return {'zed.etc': value};
case 'shippedDate':
return {'t.shipped': {
between: this.dateRange(value)}
};
case 'nickname':
return {[`t.nickname`]: {like: `%${value}%`}};
case 'zoneFk':
case 'totalWithVat':
return {[`t.${param}`]: value};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
preview(ticket) {
this.selectedTicket = ticket;
this.$.summary.show();
}
autoRefresh(value) {
if (value)
this.refreshTimer = setInterval(() => this.$.model.refresh(), 120000);
else {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
ngModule.vnComponent('vnMonitorSalesTickets', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,133 +0,0 @@
import './index.js';
describe('Component vnMonitorSalesTickets', () => {
let controller;
let $window;
let tickets = [{
id: 1,
clientFk: 1,
checked: false,
totalWithVat: 10.5
}, {
id: 2,
clientFk: 1,
checked: true,
totalWithVat: 20.5
}, {
id: 3,
clientFk: 1,
checked: true,
totalWithVat: 30
}];
beforeEach(ngModule('monitor'));
beforeEach(inject(($componentController, _$window_) => {
$window = _$window_;
const $element = angular.element('<vn-monitor-sales-tickets></vn-monitor-sales-tickets>');
controller = $componentController('vnMonitorSalesTickets', {$element});
}));
describe('fetchParams()', () => {
it('should return a range of dates with passed scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
const from = Date.vnNew();
from.setHours(0, 0, 0, 0);
const to = new Date(from.getTime());
to.setDate(to.getDate() + params.scopeDays);
to.setHours(23, 59, 59, 999);
const expectedParams = {
from,
scopeDays: params.scopeDays,
to
};
expect(params).toEqual(expectedParams);
});
it('should return default value for scope days', () => {
let params = controller.fetchParams({
scopeDays: 1
});
expect(params.scopeDays).toEqual(1);
});
it('should return the given scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
expect(params.scopeDays).toEqual(2);
});
});
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let today = Date.vnNew();
let result = controller.compareDate(today);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = Date.vnNew();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = Date.vnNew();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('totalPriceColor()', () => {
it('should return "warning" when the ticket amount is less than 50€', () => {
const result = controller.totalPriceColor({totalWithVat: '8.50'});
expect(result).toEqual('warning');
});
});
describe('dateRange()', () => {
it('should return two dates with the hours at the start and end of the given date', () => {
const now = Date.vnNew();
const today = now.getDate();
const dateRange = controller.dateRange(now);
const start = dateRange[0].toString();
const end = dateRange[1].toString();
expect(start).toContain(today);
expect(start).toContain('00:00:00');
expect(end).toContain(today);
expect(end).toContain('23:59:59');
});
});
describe('preview()', () => {
it('should show the dialog summary', () => {
controller.$.summary = {show: () => {}};
jest.spyOn(controller.$.summary, 'show');
let event = new MouseEvent('click', {
view: $window,
bubbles: true,
cancelable: true
});
controller.preview(event, tickets[0]);
expect(controller.$.summary.show).toHaveBeenCalledWith();
});
});
});

View File

@ -1,47 +0,0 @@
@import "variables";
vn-monitor-sales-tickets {
@media screen and (max-width: 1440px) {
.expendable {
display: none;
}
}
vn-th.icon-field,
vn-th.icon-field *,
vn-td.icon-field,
vn-td.icon-field * {
padding: 0;
max-width: 50px
}
vn-th[field="nickname"],
vn-td[name="nickname"] {
min-width: 250px
}
vn-td.icon-field > vn-icon {
margin-left: 3px;
margin-right: 3px;
}
vn-table.scrollable.lg {
height: 736px
}
tbody tr[ng-repeat]:focus {
background-color: $color-primary-light
}
.highRisk i {
color: $color-alert
}
td[name="nickname"] {
max-width: 200px
}
td[name="zone"] {
max-width: 150px
}
}

Some files were not shown because too many files have changed in this diff Show More