Merge branch 'dev' into 2940-invoiceInTax-section
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2021-06-15 09:09:08 +02:00
commit 5369e8d97e
69 changed files with 1343 additions and 740 deletions

View File

@ -2348,4 +2348,25 @@ INSERT INTO `vn`.`zoneAgencyMode`(`id`, `agencyModeFk`, `zoneFk`)
(1, 1, 1), (1, 1, 1),
(2, 1, 2), (2, 1, 2),
(3, 6, 5), (3, 6, 5),
(4, 7, 1); (4, 7, 1);
INSERT INTO `vn`.`expeditionTruck` (`id`, `ETD`, `description`)
VALUES
(1, CONCAT(YEAR(DATE_ADD(CURDATE(), INTERVAL +3 YEAR))), 'Best truck in fleet');
INSERT INTO `vn`.`expeditionPallet` (`id`, `truckFk`, `built`, `position`, `isPrint`)
VALUES
(1, 1, CURDATE(), 1, 1);
INSERT INTO `vn`.`expeditionScan` (`id`, `expeditionFk`, `scanned`, `palletFk`)
VALUES
(1, 1, CURDATE(), 1),
(2, 2, CURDATE(), 1),
(3, 3, CURDATE(), 1),
(4, 4, CURDATE(), 1),
(5, 5, CURDATE(), 1),
(6, 6, CURDATE(), 1),
(7, 7, CURDATE(), 1),
(8, 8, CURDATE(), 1),
(9, 9, CURDATE(), 1),
(10, 10, CURDATE(), 1);

View File

@ -474,7 +474,7 @@ export default {
advancedSearchDaysOnward: 'vn-ticket-search-panel vn-input-number[ng-model="filter.scopeDays"]', advancedSearchDaysOnward: 'vn-ticket-search-panel vn-input-number[ng-model="filter.scopeDays"]',
advancedSearchClient: 'vn-ticket-search-panel vn-textfield[ng-model="filter.clientFk"]', advancedSearchClient: 'vn-ticket-search-panel vn-textfield[ng-model="filter.clientFk"]',
advancedSearchButton: 'vn-ticket-search-panel button[type=submit]', advancedSearchButton: 'vn-ticket-search-panel button[type=submit]',
newTicketButton: 'vn-ticket-index a[ui-sref="ticket.create"]', newTicketButton: 'vn-ticket-index vn-button[icon="add"]',
searchResult: 'vn-ticket-index vn-card > vn-table > div > vn-tbody > a.vn-tr', searchResult: 'vn-ticket-index vn-card > vn-table > div > vn-tbody > a.vn-tr',
firstTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > vn-check', firstTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > vn-check',
secondTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(2) > vn-td:nth-child(1) > vn-check', secondTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(2) > vn-td:nth-child(1) > vn-check',
@ -888,7 +888,7 @@ export default {
}, },
workerCalendar: { workerCalendar: {
year: 'vn-worker-calendar vn-autocomplete[ng-model="$ctrl.year"]', year: 'vn-worker-calendar vn-autocomplete[ng-model="$ctrl.year"]',
totalHolidaysUsed: 'vn-worker-calendar div.totalBox > div', totalHolidaysUsed: 'vn-worker-calendar div.totalBox:first-child > div',
penultimateMondayOfJanuary: 'vn-worker-calendar vn-calendar:nth-child(2) section:nth-child(22) > div', penultimateMondayOfJanuary: 'vn-worker-calendar vn-calendar:nth-child(2) section:nth-child(22) > div',
lastMondayOfMarch: 'vn-worker-calendar vn-calendar:nth-child(4) section:nth-child(29) > div', lastMondayOfMarch: 'vn-worker-calendar vn-calendar:nth-child(4) section:nth-child(29) > div',
fistMondayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(8) > div', fistMondayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(8) > div',
@ -896,11 +896,11 @@ export default {
secondTuesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(16) > div', secondTuesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(16) > div',
secondWednesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(17) > div', secondWednesdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(17) > div',
secondThursdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(18) > div', secondThursdayOfMay: 'vn-worker-calendar vn-calendar:nth-child(6) section:nth-child(18) > div',
holidays: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(1)', holidays: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(1)',
absence: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(2)', absence: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(2)',
halfHoliday: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(3)', halfHoliday: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(3)',
furlough: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(4)', furlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(4)',
halfFurlough: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(5)', halfFurlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(5)',
}, },
invoiceOutIndex: { invoiceOutIndex: {
topbarSearch: 'vn-searchbar', topbarSearch: 'vn-searchbar',

View File

@ -26,7 +26,8 @@ describe('Travel create path', () => {
it('should fill the reference, agency and ship date then save the form', async() => { it('should fill the reference, agency and ship date then save the form', async() => {
await page.write(selectors.travelIndex.reference, 'Testing reference'); await page.write(selectors.travelIndex.reference, 'Testing reference');
await page.autocompleteSearch(selectors.travelIndex.agency, 'inhouse pickup'); await page.autocompleteSearch(selectors.travelIndex.agency, 'inhouse pickup');
await page.pickDate(selectors.travelIndex.shipDate, date); await page.pickDate(selectors.travelIndex.shipDate, date); // this line autocompletes another 3 fields
await page.waitForTimeout(1000);
await page.waitToClick(selectors.travelIndex.save); await page.waitToClick(selectors.travelIndex.save);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -35,8 +36,6 @@ describe('Travel create path', () => {
}); });
it('should check the user was redirected to the travel basic data upon creation', async() => { it('should check the user was redirected to the travel basic data upon creation', async() => {
// backup code for further intermitences still on track.
// await page.screenshot({path: 'e2e/paths/10-travel/error.jpeg', type: 'jpeg'});
await page.waitForState('travel.card.basicData'); await page.waitForState('travel.card.basicData');
}); });

View File

@ -11,7 +11,7 @@ import './style.scss';
* @property {String} valueField The data field name that should be used as value * @property {String} valueField The data field name that should be used as value
* @property {Array} data Static data for the autocomplete * @property {Array} data Static data for the autocomplete
* @property {Object} intialData An initial data to avoid the server request used to get the selection * @property {Object} intialData An initial data to avoid the server request used to get the selection
* @property {Boolean} multiple Wether to allow multiple selection * @property {Boolean} multiple Whether to allow multiple selection
* @property {Object} selection Current object selected * @property {Object} selection Current object selected
* *
* @event change Thrown when value is changed * @event change Thrown when value is changed

View File

@ -17,7 +17,7 @@ export default class Popup extends Component {
} }
/** /**
* @type {Boolean} Wether to show or hide the popup. * @type {Boolean} Whether to show or hide the popup.
*/ */
get shown() { get shown() {
return this._shown; return this._shown;

View File

@ -102,7 +102,7 @@ vn-table {
& > vn-one { & > vn-one {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-size: 0.75rem; font-size: 1rem;
} }
& > vn-one:nth-child(2) h3 { & > vn-one:nth-child(2) h3 {

View File

@ -180,5 +180,6 @@
"This genus already exist": "Este genus ya existe", "This genus already exist": "Este genus ya existe",
"This specie already exist": "Esta especie ya existe", "This specie already exist": "Esta especie ya existe",
"Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Client assignment has changed": "He cambiado el comercial ~*\"<{{previousWorkerName}}>\"*~ por *\"<{{currentWorkerName}}>\"* del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"None": "Ninguno" "None": "Ninguno",
"The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada"
} }

View File

@ -89,7 +89,7 @@ module.exports = Self => {
const newBuy = await models.Buy.create(ctx.args, myOptions); const newBuy = await models.Buy.create(ctx.args, myOptions);
let filter = { const filter = {
fields: [ fields: [
'id', 'id',
'itemFk', 'itemFk',
@ -136,7 +136,7 @@ module.exports = Self => {
} }
}; };
let stmts = []; const stmts = [];
let stmt; let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc');

View File

@ -32,7 +32,7 @@ module.exports = Self => {
} }
try { try {
let promises = []; const promises = [];
for (let buy of ctx.args.buys) { for (let buy of ctx.args.buys) {
const buysToDelete = models.Buy.destroyById(buy.id, myOptions); const buysToDelete = models.Buy.destroyById(buy.id, myOptions);
promises.push(buysToDelete); promises.push(buysToDelete);

View File

@ -30,7 +30,18 @@ module.exports = Self => {
} }
}); });
Self.editLatestBuys = async(field, newValue, lines) => { Self.editLatestBuys = async(field, newValue, lines, options) => {
let tx;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
let modelName; let modelName;
let identifier; let identifier;
@ -60,28 +71,27 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const model = models[modelName]; const model = models[modelName];
let tx = await model.beginTransaction({});
try { try {
let promises = []; let promises = [];
let options = {transaction: tx};
let targets = lines.map(line => { const targets = lines.map(line => {
return line[identifier]; return line[identifier];
}); });
let value = {}; const value = {};
value[field] = newValue; value[field] = newValue;
// intentarlo con updateAll
for (let target of targets) for (let target of targets)
promises.push(model.upsertWithWhere({id: target}, value, options)); promises.push(model.upsertWithWhere({id: target}, value, myOptions));
await Promise.all(promises); const result = await Promise.all(promises, myOptions);
await tx.commit();
} catch (error) { if (tx) await tx.commit();
await tx.rollback();
throw error; return result;
} catch (e) {
if (tx) await tx.rollback();
throw e;
} }
}; };
}; };

View File

@ -13,71 +13,85 @@ module.exports = Self => {
type: 'object', type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'search', arg: 'search',
type: 'string', type: 'string',
description: 'Searchs the entry by id', description: 'Searchs the entry by id',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'id', arg: 'id',
type: 'integer', type: 'integer',
description: 'The entry id', description: 'The entry id',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'created', arg: 'created',
type: 'date', type: 'date',
description: 'The created date to filter', description: 'The created date to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'travelFk', arg: 'travelFk',
type: 'number', type: 'number',
description: 'The travel id to filter', description: 'The travel id to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'companyFk', arg: 'companyFk',
type: 'number', type: 'number',
description: 'The company to filter', description: 'The company to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'isBooked', arg: 'isBooked',
type: 'boolean', type: 'boolean',
description: 'The isBokked filter', description: 'The isBokked filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'isConfirmed', arg: 'isConfirmed',
type: 'boolean', type: 'boolean',
description: 'The isConfirmed filter', description: 'The isConfirmed filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'isOrdered', arg: 'isOrdered',
type: 'boolean', type: 'boolean',
description: 'The isOrdered filter', description: 'The isOrdered filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'ref', arg: 'ref',
type: 'string', type: 'string',
description: 'The ref filter', description: 'The ref filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'supplierFk', arg: 'supplierFk',
type: 'number', type: 'number',
description: 'The supplier id to filter', description: 'The supplier id to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'invoiceInFk', arg: 'invoiceInFk',
type: 'number', type: 'number',
description: 'The invoiceIn id to filter', description: 'The invoiceIn id to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'currencyFk', arg: 'currencyFk',
type: 'number', type: 'number',
description: 'The currency id to filter', description: 'The currency id to filter',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'from', arg: 'from',
type: 'date', type: 'date',
description: `The from date filter` description: `The from date filter`
}, { },
{
arg: 'to', arg: 'to',
type: 'date', type: 'date',
description: `The to date filter` description: `The to date filter`
@ -93,9 +107,14 @@ module.exports = Self => {
} }
}); });
Self.filter = async(ctx, filter) => { Self.filter = async(ctx, filter, options) => {
let conn = Self.dataSource.connector; let myOptions = {};
let where = buildFilter(ctx.args, (param, value) => {
if (typeof options == 'object')
Object.assign(myOptions, options);
const conn = Self.dataSource.connector;
const where = buildFilter(ctx.args, (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
return /^\d+$/.test(value) return /^\d+$/.test(value)
@ -128,7 +147,7 @@ module.exports = Self => {
}); });
filter = mergeFilters(ctx.args.filter, {where}); filter = mergeFilters(ctx.args.filter, {where});
let stmts = []; const stmts = [];
let stmt; let stmt;
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`SELECT `SELECT
@ -163,10 +182,10 @@ module.exports = Self => {
); );
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
let itemsIndex = stmts.push(stmt) - 1; const itemsIndex = stmts.push(stmt) - 1;
let sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql, myOptions);
return itemsIndex === 0 ? result : result[itemsIndex]; return itemsIndex === 0 ? result : result[itemsIndex];
}; };
}; };

View File

@ -1,14 +1,22 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('getBuys', { Self.remoteMethod('getBuys', {
description: 'Returns buys for one entry', description: 'Returns buys for one entry',
accessType: 'READ', accessType: 'READ',
accepts: { accepts: [{
arg: 'id', arg: 'id',
type: 'number', type: 'number',
required: true, required: true,
description: 'The entry id', description: 'The entry id',
http: {source: 'path'} http: {source: 'path'}
}, },
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
}
],
returns: { returns: {
type: ['Object'], type: ['Object'],
root: true root: true
@ -19,8 +27,14 @@ module.exports = Self => {
} }
}); });
Self.getBuys = async id => { Self.getBuys = async(id, filter, options) => {
let filter = { const models = Self.app.models;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
let defaultFilter = {
where: {entryFk: id}, where: {entryFk: id},
fields: [ fields: [
'id', 'id',
@ -68,7 +82,8 @@ module.exports = Self => {
} }
}; };
let buys = await Self.app.models.Buy.find(filter); defaultFilter = mergeFilters(defaultFilter, filter);
return buys;
return models.Buy.find(defaultFilter, myOptions);
}; };
}; };

View File

@ -19,8 +19,14 @@ module.exports = Self => {
} }
}); });
Self.getEntry = async id => { Self.getEntry = async(id, options) => {
let filter = { const models = Self.app.models;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const filter = {
where: {id: id}, where: {id: id},
include: [ include: [
{ {
@ -70,7 +76,6 @@ module.exports = Self => {
], ],
}; };
let entry = await Self.app.models.Entry.findOne(filter); return models.Entry.findOne(filter, myOptions);
return entry;
}; };
}; };

View File

@ -45,8 +45,8 @@ module.exports = Self => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const args = ctx.args; const args = ctx.args;
const models = Self.app.models; const models = Self.app.models;
let tx; let tx;
if (!options.transaction) { if (!options.transaction) {
tx = await Self.beginTransaction({}); tx = await Self.beginTransaction({});
options.transaction = tx; options.transaction = tx;
@ -76,7 +76,7 @@ module.exports = Self => {
const createdBuys = await models.Buy.create(buys, options); const createdBuys = await models.Buy.create(buys, options);
const buyIds = createdBuys.map(buy => buy.id); const buyIds = createdBuys.map(buy => buy.id);
let stmts = []; const stmts = [];
let stmt; let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc');

View File

@ -24,14 +24,19 @@ module.exports = Self => {
} }
}); });
Self.importBuysPreview = async(id, buys) => { Self.importBuysPreview = async(id, buys, options) => {
const models = Self.app.models; const models = Self.app.models;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
for (let buy of buys) { for (let buy of buys) {
const packaging = await models.Packaging.findOne({ const packaging = await models.Packaging.findOne({
fields: ['id'], fields: ['id'],
where: {volume: {gte: buy.volume}}, where: {volume: {gte: buy.volume}},
order: 'volume ASC' order: 'volume ASC'
}); }, myOptions);
buy.packageFk = packaging.id; buy.packageFk = packaging.id;
} }

View File

@ -17,36 +17,44 @@ module.exports = Self => {
arg: 'search', arg: 'search',
type: 'String', type: 'String',
description: `If it's and integer searchs by id, otherwise it searchs by name`, description: `If it's and integer searchs by id, otherwise it searchs by name`,
}, { },
{
arg: 'id', arg: 'id',
type: 'Integer', type: 'Integer',
description: 'Item id', description: 'Item id',
}, { },
{
arg: 'tags', arg: 'tags',
type: ['Object'], type: ['Object'],
description: 'List of tags to filter with', description: 'List of tags to filter with',
http: {source: 'query'} http: {source: 'query'}
}, { },
{
arg: 'description', arg: 'description',
type: 'String', type: 'String',
description: 'The item description', description: 'The item description',
}, { },
{
arg: 'salesPersonFk', arg: 'salesPersonFk',
type: 'Integer', type: 'Integer',
description: 'The buyer of the item', description: 'The buyer of the item',
}, { },
{
arg: 'active', arg: 'active',
type: 'Boolean', type: 'Boolean',
description: 'Whether the item is or not active', description: 'Whether the item is or not active',
}, { },
{
arg: 'visible', arg: 'visible',
type: 'Boolean', type: 'Boolean',
description: 'Whether the item is or not visible', description: 'Whether the item is or not visible',
}, { },
{
arg: 'typeFk', arg: 'typeFk',
type: 'Integer', type: 'Integer',
description: 'Type id', description: 'Type id',
}, { },
{
arg: 'categoryFk', arg: 'categoryFk',
type: 'Integer', type: 'Integer',
description: 'Category id', description: 'Category id',
@ -62,9 +70,14 @@ module.exports = Self => {
} }
}); });
Self.latestBuysFilter = async(ctx, filter) => { Self.latestBuysFilter = async(ctx, filter, options) => {
let conn = Self.dataSource.connector; let myOptions = {};
let where = buildFilter(ctx.args, (param, value) => {
if (typeof options == 'object')
Object.assign(myOptions, options);
const conn = Self.dataSource.connector;
const where = buildFilter(ctx.args, (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
return /^\d+$/.test(value) return /^\d+$/.test(value)
@ -93,7 +106,7 @@ module.exports = Self => {
}); });
filter = mergeFilters(ctx.args.filter, {where}); filter = mergeFilters(ctx.args.filter, {where});
let stmts = []; const stmts = [];
let stmt; let stmt;
stmts.push('CALL cache.last_buy_refresh(FALSE)'); stmts.push('CALL cache.last_buy_refresh(FALSE)');
@ -179,10 +192,10 @@ module.exports = Self => {
} }
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
let buysIndex = stmts.push(stmt) - 1; const buysIndex = stmts.push(stmt) - 1;
let sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql, myOptions);
return buysIndex === 0 ? result : result[buysIndex]; return buysIndex === 0 ? result : result[buysIndex];
}; };
}; };

View File

@ -3,29 +3,32 @@ const model = app.models.Buy;
describe('Buy editLatestsBuys()', () => { describe('Buy editLatestsBuys()', () => {
it('should change the value of a given column for the selected buys', async() => { it('should change the value of a given column for the selected buys', async() => {
let ctx = { const tx = await app.models.Entry.beginTransaction({});
args: { const options = {transaction: tx};
search: 'Ranged weapon longbow 2m'
}
};
let [original] = await model.latestBuysFilter(ctx); try {
let ctx = {
args: {
search: 'Ranged weapon longbow 2m'
}
};
const field = 'size'; const [original] = await model.latestBuysFilter(ctx, null, options);
let newValue = 99;
const lines = [{itemFk: original.itemFk, id: original.id}];
await model.editLatestBuys(field, newValue, lines); const field = 'size';
const newValue = 99;
const lines = [{itemFk: original.itemFk, id: original.id}];
let [result] = await model.latestBuysFilter(ctx); await model.editLatestBuys(field, newValue, lines, options);
expect(result.size).toEqual(99); const [result] = await model.latestBuysFilter(ctx, null, options);
newValue = original.size; expect(result[field]).toEqual(newValue);
await model.editLatestBuys(field, newValue, lines);
let [restoredFixture] = await model.latestBuysFilter(ctx); await tx.rollback();
} catch (e) {
expect(restoredFixture.size).toEqual(original.size); await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -10,7 +10,7 @@ Commission: Comisión
Landed: F. entrega Landed: F. entrega
Reference: Referencia Reference: Referencia
Created: Creado Created: Creado
Booked: Facturado Booked: Contabilizada
Is inventory: Inventario Is inventory: Inventario
Notes: Notas Notes: Notas
Status: Estado Status: Estado

View File

@ -1,3 +1,10 @@
<vn-crud-model
vn-id="buysModel"
url="Entries/{{$ctrl.$params.id}}/getBuys"
limit="5"
data="buys"
auto-load="true">
</vn-crud-model>
<vn-card class="summary"> <vn-card class="summary">
<h5> <h5>
<a ng-if="::$ctrl.entryData.id" <a ng-if="::$ctrl.entryData.id"
@ -95,7 +102,7 @@
<th translate center expand field="price3">Packing price</th> <th translate center expand field="price3">Packing price</th>
</tr> </tr>
</thead> </thead>
<tbody ng-repeat="line in $ctrl.buys"> <tbody ng-repeat="line in buys">
<tr> <tr>
<td center title="{{::line.quantity}}">{{::line.quantity}}</td> <td center title="{{::line.quantity}}">{{::line.quantity}}</td>
<td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td> <td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td>
@ -156,6 +163,10 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<vn-pagination
model="buysModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto> </vn-auto>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -10,10 +10,8 @@ class Controller extends Summary {
set entry(value) { set entry(value) {
this._entry = value; this._entry = value;
if (value && value.id) { if (value && value.id)
this.getEntryData(); this.getEntryData();
this.getBuys();
}
} }
getEntryData() { getEntryData() {
@ -21,12 +19,6 @@ class Controller extends Summary {
this.entryData = response.data; this.entryData = response.data;
}); });
} }
getBuys() {
return this.$http.get(`Entries/${this.entry.id}/getBuys`).then(response => {
this.buys = response.data;
});
}
} }
ngModule.vnComponent('vnEntrySummary', { ngModule.vnComponent('vnEntrySummary', {

View File

@ -46,20 +46,4 @@ describe('component vnEntrySummary', () => {
expect(controller.entryData).toEqual('I am the entryData'); expect(controller.entryData).toEqual('I am the entryData');
}); });
}); });
describe('getBuys()', () => {
it('should perform a get asking for the buys of an entry', () => {
controller._entry = {id: 999};
const thatQuery = `Entries/${controller._entry.id}/getEntry`;
const query = `Entries/${controller._entry.id}/getBuys`;
$httpBackend.whenGET(thatQuery).respond('My Entries');
$httpBackend.expectGET(query).respond('Some buys');
controller.getBuys();
$httpBackend.flush();
expect(controller.buys).toEqual('Some buys');
});
});
}); });

View File

@ -139,6 +139,7 @@ module.exports = Self => {
ii.supplierRef, ii.supplierRef,
ii.docFk AS dmsFk, ii.docFk AS dmsFk,
ii.supplierFk, ii.supplierFk,
ii.expenceFkDeductible deductibleExpenseFk,
s.name AS supplierName, s.name AS supplierName,
s.account, s.account,
SUM(iid.amount) AS amount, SUM(iid.amount) AS amount,

View File

@ -57,6 +57,19 @@ module.exports = Self => {
} }
}] }]
} }
},
{
relation: 'expenseDeductible',
scope: {
fields: ['id', 'name', 'taxTypeFk']
}
},
{
relation: 'currency',
scope: {
fields: ['id', 'name']
}
} }
] ]
}; };

View File

@ -33,9 +33,6 @@
"isBooked": { "isBooked": {
"type": "boolean" "type": "boolean"
}, },
"isVatDeductible": {
"type": "boolean"
},
"booked": { "booked": {
"type": "date" "type": "date"
}, },
@ -50,6 +47,12 @@
"mysql": { "mysql": {
"columnName": "docFk" "columnName": "docFk"
} }
},
"deductibleExpenseFk": {
"type": "number",
"mysql": {
"columnName": "expenceFkDeductible"
}
} }
}, },
"relations": { "relations": {
@ -68,6 +71,11 @@
"model": "SageWithholding", "model": "SageWithholding",
"foreignKey": "withholdingSageFk" "foreignKey": "withholdingSageFk"
}, },
"expenseDeductible": {
"type": "belongsTo",
"model": "Expense",
"foreignKey": "deductibleExpenseFk"
},
"company": { "company": {
"type": "belongsTo", "type": "belongsTo",
"model": "Company", "model": "Company",

View File

@ -7,21 +7,6 @@
</vn-watcher> </vn-watcher>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md"> <form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-date-picker
vn-one
label="Expedition date"
ng-model="$ctrl.invoiceIn.issued"
vn-focus
rule>
</vn-date-picker>
<vn-date-picker
vn-one
label="Operation date"
ng-model="$ctrl.invoiceIn.operated"
rule>
</vn-date-picker>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
@ -44,6 +29,35 @@
rule> rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-date-picker
vn-one
label="Expedition date"
ng-model="$ctrl.invoiceIn.issued"
vn-focus
rule>
</vn-date-picker>
<vn-date-picker
vn-one
label="Operation date"
ng-model="$ctrl.invoiceIn.operated"
rule>
</vn-date-picker>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Undeductible VAT"
ng-model="$ctrl.invoiceIn.deductibleExpenseFk"
value-field="id"
order="name"
url="Expenses"
fields="['id','name']"
rule>
<tpl-item>
{{id}} - {{name}}
</tpl-item>
</vn-datalist>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-date-picker <vn-date-picker
vn-one vn-one

View File

@ -18,6 +18,8 @@
</vn-label-value> </vn-label-value>
<vn-label-value label="Supplier ref" value="{{$ctrl.summary.supplierRef}}"> <vn-label-value label="Supplier ref" value="{{$ctrl.summary.supplierRef}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Currency" value="{{$ctrl.summary.currency.name}}">
</vn-label-value>
<vn-label-value label="Doc number" value="{{$ctrl.summary.serial}}/{{$ctrl.summary.serialNumber}}"> <vn-label-value label="Doc number" value="{{$ctrl.summary.serial}}/{{$ctrl.summary.serialNumber}}">
</vn-label-value> </vn-label-value>
</vn-one> </vn-one>
@ -34,11 +36,11 @@
<vn-one> <vn-one>
<vn-label-value label="Sage withholding" value="{{$ctrl.summary.sageWithholding.withholding}}"> <vn-label-value label="Sage withholding" value="{{$ctrl.summary.sageWithholding.withholding}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Undeductible VAT" value="{{$ctrl.summary.expenseDeductible.name}}">
</vn-label-value>
<vn-label-value label="Company" value="{{$ctrl.summary.company.code}}"> <vn-label-value label="Company" value="{{$ctrl.summary.company.code}}">
</vn-label-value> </vn-label-value>
<vn-vertical> <vn-vertical>
<vn-check label="Deductible" ng-model="$ctrl.summary.isVatDeductible" disabled="true">
</vn-check>
<vn-check label="Booked" ng-model="$ctrl.summary.isBooked" disabled="true"> <vn-check label="Booked" ng-model="$ctrl.summary.isBooked" disabled="true">
</vn-check> </vn-check>
</vn-vertical> </vn-vertical>

View File

@ -7,4 +7,4 @@ Booked date: Fecha contable
Accounted date: Fecha contable Accounted date: Fecha contable
Doc number: Numero documento Doc number: Numero documento
Sage withholding: Retención sage Sage withholding: Retención sage
Deductible: Deducible Undeductible VAT: Iva no deducible

View File

@ -0,0 +1,55 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('getTickets', {
description: 'Returns tickets for one invoiceOut',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceOut id',
http: {source: 'path'}
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/getTickets`,
verb: 'GET'
}
});
Self.getTickets = async(id, filter, options) => {
const models = Self.app.models;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const invoiceOut = await models.InvoiceOut.findById(id, {fields: 'ref'}, myOptions);
let defaultFilter = {
where: {refFk: invoiceOut.ref},
fields: [
'id',
'nickname',
'shipped',
'totalWithVat',
'clientFk'
]
};
defaultFilter = mergeFilters(defaultFilter, filter);
return models.Ticket.find(defaultFilter, myOptions);
};
};

View File

@ -0,0 +1,10 @@
const app = require('vn-loopback/server/server');
describe('entry getTickets()', () => {
const invoiceOutId = 4;
it('should get the ticket of an invoiceOut', async() => {
const result = await app.models.InvoiceOut.getTickets(invoiceOutId);
expect(result.length).toEqual(1);
});
});

View File

@ -7,14 +7,6 @@ describe('invoiceOut summary()', () => {
expect(result.invoiceOut.id).toEqual(1); expect(result.invoiceOut.id).toEqual(1);
}); });
it(`should return a summary object containing data from it's tickets`, async() => {
const summary = await app.models.InvoiceOut.summary(1);
const tickets = summary.invoiceOut.tickets();
expect(summary.invoiceOut.ref).toEqual('T1111111');
expect(tickets.length).toEqual(2);
});
it(`should return a summary object containing it's supplier country`, async() => { it(`should return a summary object containing it's supplier country`, async() => {
const summary = await app.models.InvoiceOut.summary(1); const summary = await app.models.InvoiceOut.summary(1);
const supplier = summary.invoiceOut.supplier(); const supplier = summary.invoiceOut.supplier();

View File

@ -1,5 +1,3 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('summary', { Self.remoteMethod('summary', {
description: 'The invoiceOut summary', description: 'The invoiceOut summary',
@ -22,7 +20,6 @@ module.exports = Self => {
}); });
Self.summary = async id => { Self.summary = async id => {
const conn = Self.dataSource.connector;
let summary = {}; let summary = {};
const filter = { const filter = {
@ -57,54 +54,20 @@ module.exports = Self => {
scope: { scope: {
fields: ['id', 'socialName'] fields: ['id', 'socialName']
} }
},
{
relation: 'tickets'
} }
] ]
}; };
summary.invoiceOut = await Self.app.models.InvoiceOut.findOne(filter); summary.invoiceOut = await Self.app.models.InvoiceOut.findOne(filter);
let stmts = []; const invoiceOutTaxes = await Self.rawSql(`
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.ticket');
stmt = new ParameterizedSQL(`
CREATE TEMPORARY TABLE tmp.ticket
(INDEX (ticketFk)) ENGINE = MEMORY
SELECT id ticketFk FROM vn.ticket WHERE refFk=?`, [summary.invoiceOut.ref]);
stmts.push(stmt);
stmts.push('CALL ticketGetTotal()');
let ticketTotalsIndex = stmts.push('SELECT * FROM tmp.ticketTotal') - 1;
stmt = new ParameterizedSQL(`
SELECT iot.* , pgc.*, IF(pe.equFk IS NULL, taxableBase, 0) AS Base, pgc.rate / 100 as vatPercent SELECT iot.* , pgc.*, IF(pe.equFk IS NULL, taxableBase, 0) AS Base, pgc.rate / 100 as vatPercent
FROM vn.invoiceOutTax iot FROM vn.invoiceOutTax iot
JOIN vn.pgc ON pgc.code = iot.pgcFk JOIN vn.pgc ON pgc.code = iot.pgcFk
LEFT JOIN vn.pgcEqu pe ON pe.equFk = pgc.code LEFT JOIN vn.pgcEqu pe ON pe.equFk = pgc.code
WHERE invoiceOutFk = ?`, [summary.invoiceOut.id]); WHERE invoiceOutFk = ?`, [summary.invoiceOut.id]);
let invoiceOutTaxesIndex = stmts.push(stmt) - 1;
stmts.push( summary.invoiceOut.taxesBreakdown = invoiceOutTaxes;
`DROP TEMPORARY TABLE
tmp.ticket,
tmp.ticketTotal`);
let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql);
totalMap = {};
for (ticketTotal of result[ticketTotalsIndex])
totalMap[ticketTotal.ticketFk] = ticketTotal.total;
summary.invoiceOut.tickets().forEach(ticket => {
ticket.total = totalMap[ticket.id];
});
summary.invoiceOut.taxesBreakdown = result[invoiceOutTaxesIndex];
return summary; return summary;
}; };

View File

@ -1,6 +1,7 @@
module.exports = Self => { module.exports = Self => {
require('../methods/invoiceOut/filter')(Self); require('../methods/invoiceOut/filter')(Self);
require('../methods/invoiceOut/summary')(Self); require('../methods/invoiceOut/summary')(Self);
require('../methods/invoiceOut/getTickets')(Self);
require('../methods/invoiceOut/download')(Self); require('../methods/invoiceOut/download')(Self);
require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/delete')(Self);
require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/book')(Self);

View File

@ -1,3 +1,10 @@
<vn-crud-model
vn-id="ticketsModel"
url="InvoiceOuts/{{$ctrl.$params.id}}/getTickets"
limit="10"
data="tickets"
auto-load="true">
</vn-crud-model>
<vn-card class="summary"> <vn-card class="summary">
<h5> <h5>
<a ng-if="::$ctrl.summary.invoiceOut.id" <a ng-if="::$ctrl.summary.invoiceOut.id"
@ -59,7 +66,7 @@
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="ticket in $ctrl.summary.invoiceOut.tickets"> <vn-tr ng-repeat="ticket in tickets">
<vn-td number> <vn-td number>
<span <span
ng-click="ticketDescriptor.show($event, ticket.id)" ng-click="ticketDescriptor.show($event, ticket.id)"
@ -75,10 +82,14 @@
</span> </span>
</vn-td> </vn-td>
<vn-td expand>{{ticket.shipped | date: 'dd/MM/yyyy' | dashIfEmpty}}</vn-td> <vn-td expand>{{ticket.shipped | date: 'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td number>{{ticket.total | currency: 'EUR': 2}}</vn-td> <vn-td number>{{ticket.totalWithVat | currency: 'EUR': 2}}</vn-td>
</vn-tr> </vn-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>
<vn-pagination
model="ticketsModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto> </vn-auto>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -9,14 +9,17 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"name": { "name": {
"type": "String" "type": "String"
}, },
"isWithheld": { "isWithheld": {
"type": "Number" "type": "number"
},
"taxTypeFk": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -3,6 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('route updateVolume()', () => { describe('route updateVolume()', () => {
const routeId = 1; const routeId = 1;
const ticketId = 14;
const userId = 50; const userId = 50;
const activeCtx = { const activeCtx = {
accessToken: {userId: userId}, accessToken: {userId: userId},
@ -13,31 +14,39 @@ describe('route updateVolume()', () => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
const route = await app.models.Route.findById(routeId);
expect(route.m3).toEqual(1.8); const tx = await app.models.Ticket.beginTransaction({});
const ticket = await app.models.Ticket.findById(14); try {
await ticket.updateAttributes({routeFk: routeId}); const options = {transaction: tx};
await app.models.Route.updateVolume(ctx, routeId);
const updatedRoute = await app.models.Route.findById(routeId); const route = await app.models.Route.findById(routeId, null, options);
expect(updatedRoute.m3).not.toEqual(route.m3); expect(route.m3).toEqual(1.8);
const logs = await app.models.RouteLog.find({fields: ['id', 'newInstance']}); const ticket = await app.models.Ticket.findById(ticketId, null, options);
await ticket.updateAttributes({routeFk: routeId}, options);
await app.models.Route.updateVolume(ctx, routeId, options);
const m3Log = logs.filter(log => { const updatedRoute = await app.models.Route.findById(routeId, null, options);
if (log.newInstance)
return log.newInstance.m3 === updatedRoute.m3;
});
const logIdToDestroy = m3Log[0].id;
expect(m3Log.length).toEqual(1); expect(updatedRoute.m3).not.toEqual(route.m3);
// restores const logs = await app.models.RouteLog.find({
await ticket.updateAttributes({routeFk: null}); fields: ['id', 'newInstance']
await route.updateAttributes({m3: 1.8}); }, options);
await app.models.RouteLog.destroyById(logIdToDestroy);
const m3Log = logs.filter(log => {
if (log.newInstance)
return log.newInstance.m3 === updatedRoute.m3;
});
expect(m3Log.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -19,24 +19,44 @@ module.exports = Self => {
} }
}); });
Self.updateVolume = async(ctx, id) => { Self.updateVolume = async(ctx, id, options) => {
let query = `CALL vn.routeUpdateM3(?)`; const userId = ctx.req.accessToken.userId;
let userId = ctx.req.accessToken.userId; const models = Self.app.models;
let originalRoute = await Self.app.models.Route.findById(id);
await Self.rawSql(query, [id]); let tx;
let updatedRoute = await Self.app.models.Route.findById(id); let myOptions = {};
let logRecord = { if (typeof options == 'object')
originFk: id, Object.assign(myOptions, options);
userFk: userId,
action: 'update',
changedModel: 'Route',
changedModelId: id,
oldInstance: {m3: originalRoute.m3},
newInstance: {m3: updatedRoute.m3}
};
return await Self.app.models.RouteLog.create(logRecord); if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const originalRoute = await models.Route.findById(id, null, myOptions);
await Self.rawSql(`CALL vn.routeUpdateM3(?)`, [id], myOptions);
const updatedRoute = await models.Route.findById(id, null, myOptions);
await models.RouteLog.create({
originFk: id,
userFk: userId,
action: 'update',
changedModel: 'Route',
changedModelId: id,
oldInstance: {m3: originalRoute.m3},
newInstance: {m3: updatedRoute.m3}
}, myOptions);
if (tx) await tx.commit();
return updatedRoute;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -149,11 +149,13 @@
ng-model="ticket.checked"> ng-model="ticket.checked">
</vn-check> </vn-check>
</vn-td> </vn-td>
<vn-td number>{{::ticket.id}}</vn-td>
<vn-td number> <vn-td number>
<span <span class="link" ng-click="ticketDescriptor.show($event, ticket.id)">
ng-click="::$ctrl.showClientDescriptor($event, ticket.clientFk)" {{::ticket.id}}
class="link"> </span>
</vn-td>
<vn-td number>
<span class="link" ng-click="clientDescriptor.show($event, ticket.clientFk)">
{{::ticket.nickname}} {{::ticket.nickname}}
</span> </span>
</vn-td> </vn-td>
@ -180,3 +182,9 @@
vn-bind="+" vn-bind="+"
fixed-bottom-right> fixed-bottom-right>
</vn-float-button> </vn-float-button>
<vn-ticket-descriptor-popover
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>

View File

@ -39,14 +39,19 @@ module.exports = Self => {
e.externalId, e.externalId,
i3.name packagingName, i3.name packagingName,
i3.id packagingItemFk, i3.id packagingItemFk,
e.packagingFk e.packagingFk,
es.workerFk expeditionScanWorkerFk,
su.nickname scannerUserNickname,
es.scanned
FROM FROM
vn.expedition e vn.expedition e
LEFT JOIN vn.item i2 ON i2.id = e.itemFk LEFT JOIN vn.item i2 ON i2.id = e.itemFk
INNER JOIN vn.item i1 ON i1.id = e.isBox INNER JOIN vn.item i1 ON i1.id = e.isBox
LEFT JOIN vn.packaging p ON p.id = e.packagingFk LEFT JOIN vn.packaging p ON p.id = e.packagingFk
LEFT JOIN vn.item i3 ON i3.id = p.itemFk LEFT JOIN vn.item i3 ON i3.id = p.itemFk
LEFT JOIN account.user u ON u.id = e.workerFk LEFT JOIN account.user u ON u.id = e.workerFk
LEFT JOIN vn.expeditionScan es ON es.expeditionFk = e.id
LEFT JOIN account.user su ON su.id = es.workerFk
`); `);
stmt.merge(Self.buildSuffix(filter, 'e')); stmt.merge(Self.buildSuffix(filter, 'e'));

View File

@ -12,79 +12,98 @@ module.exports = Self => {
arg: 'ctx', arg: 'ctx',
type: 'object', type: 'object',
http: {source: 'context'} http: {source: 'context'}
}, { },
{
arg: 'filter', arg: 'filter',
type: 'object', type: 'object',
description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string` description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string`
}, { },
{
arg: 'search', arg: 'search',
type: 'string', type: 'string',
description: `If it's and number searchs by id, otherwise it searchs by nickname` description: `If it's and number searchs by id, otherwise it searchs by nickname`
}, { },
{
arg: 'from', arg: 'from',
type: 'date', type: 'date',
description: `The from date filter` description: `The from date filter`
}, { },
{
arg: 'to', arg: 'to',
type: 'date', type: 'date',
description: `The to date filter` description: `The to date filter`
}, { },
{
arg: 'nickname', arg: 'nickname',
type: 'string', type: 'string',
description: `The nickname filter` description: `The nickname filter`
}, { },
{
arg: 'id', arg: 'id',
type: 'number', type: 'number',
description: `The ticket id filter` description: `The ticket id filter`
}, { },
{
arg: 'clientFk', arg: 'clientFk',
type: 'number', type: 'number',
description: `The client id filter` description: `The client id filter`
}, { },
{
arg: 'agencyModeFk', arg: 'agencyModeFk',
type: 'number', type: 'number',
description: `The agency mode id filter` description: `The agency mode id filter`
}, { },
{
arg: 'warehouseFk', arg: 'warehouseFk',
type: 'number', type: 'number',
description: `The warehouse id filter` description: `The warehouse id filter`
}, { },
{
arg: 'salesPersonFk', arg: 'salesPersonFk',
type: 'number', type: 'number',
description: `The salesperson id filter` description: `The salesperson id filter`
}, { },
{
arg: 'provinceFk', arg: 'provinceFk',
type: 'number', type: 'number',
description: `The province id filter` description: `The province id filter`
}, { },
{
arg: 'stateFk', arg: 'stateFk',
type: 'number', type: 'number',
description: `The state id filter` description: `The state id filter`
}, { },
{
arg: 'myTeam', arg: 'myTeam',
type: 'boolean', type: 'boolean',
description: `Whether to show only tickets for the current logged user team (For now it shows only the current user tickets)` description: `Whether to show only tickets for the current logged user team (For now it shows only the current user tickets)`
}, { },
{
arg: 'problems', arg: 'problems',
type: 'boolean', type: 'boolean',
description: `Whether to show only tickets with problems` description: `Whether to show only tickets with problems`
}, { },
{
arg: 'pending', arg: 'pending',
type: 'boolean', type: 'boolean',
description: `Whether to show only tickets with state 'Pending'` description: `Whether to show only tickets with state 'Pending'`
}, { },
{
arg: 'mine', arg: 'mine',
type: 'boolean', type: 'boolean',
description: `Whether to show only tickets for the current logged user` description: `Whether to show only tickets for the current logged user`
}, { },
{
arg: 'orderFk', arg: 'orderFk',
type: 'number', type: 'number',
description: `The order id filter` description: `The order id filter`
}, { },
{
arg: 'refFk', arg: 'refFk',
type: 'string', type: 'string',
description: `The invoice reference filter` description: `The invoice reference filter`
}, { },
{
arg: 'alertLevel', arg: 'alertLevel',
type: 'number', type: 'number',
description: `The alert level of the tickets` description: `The alert level of the tickets`

View File

@ -63,7 +63,7 @@ module.exports = Self => {
}, { }, {
relation: 'address', relation: 'address',
scope: { scope: {
fields: ['street', 'city', 'provinceFk', 'phone', 'mobile'], fields: ['street', 'city', 'provinceFk', 'phone', 'mobile', 'postalCode'],
include: { include: {
relation: 'province', relation: 'province',
scope: { scope: {

View File

@ -2,4 +2,5 @@ No delivery zone available for this landing date: No hay una zona de reparto dis
No delivery zone available for this shipping date: No hay una zona de reparto disponible para la fecha de preparación seleccionada No delivery zone available for this shipping date: No hay una zona de reparto disponible para la fecha de preparación seleccionada
No delivery zone available for this parameters: No hay una zona de reparto disponible con estos parámetros No delivery zone available for this parameters: No hay una zona de reparto disponible con estos parámetros
Deleted: Eliminado Deleted: Eliminado
Zone: Zona Zone: Zona
Edit address: Editar dirección

View File

@ -101,7 +101,7 @@ class Controller extends Component {
this.$http.get(`Agencies/getAgenciesWithWarehouse`, {params}).then(res => { this.$http.get(`Agencies/getAgenciesWithWarehouse`, {params}).then(res => {
this.agencies = res.data; this.agencies = res.data;
const defaultAgency = this.agencies.find(agency=> { const defaultAgency = this.agencies.find(agency => {
return agency.agencyModeFk == this.defaultAddress.agencyModeFk; return agency.agencyModeFk == this.defaultAddress.agencyModeFk;
}); });
if (defaultAgency) if (defaultAgency)

View File

@ -19,8 +19,10 @@
<vn-th field="isBox">Package type</vn-th> <vn-th field="isBox">Package type</vn-th>
<vn-th field="counter" number>Counter</vn-th> <vn-th field="counter" number>Counter</vn-th>
<vn-th field="externalId" number>externalId</vn-th> <vn-th field="externalId" number>externalId</vn-th>
<vn-th field="worker">Worker</vn-th> <vn-th field="worker">Packager</vn-th>
<vn-th field="created" expand>Created</vn-th> <vn-th field="created" expand>Created</vn-th>
<vn-th field="scanned" expand>Scanned</vn-th>
<vn-th field="expeditionScanWorkerFk">Palletizer</vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
@ -51,6 +53,12 @@
</span> </span>
</vn-td> </vn-td>
<vn-td expand>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td> <vn-td expand>{{::expedition.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td expand>{{::expedition.scanned | date:'dd/MM/yyyy HH:mm'}}</vn-td>
<vn-td expand>
<span class="link" ng-click="workerDescriptor.show($event, expedition.expeditionScanWorkerFk)">
{{::expedition.scannerUserNickname | dashIfEmpty}}
</span>
</vn-td>
</vn-tr> </vn-tr>
</vn-tbody> </vn-tbody>
</vn-table> </vn-table>

View File

@ -166,7 +166,7 @@
vn-tooltip="Payment on account..." vn-tooltip="Payment on account..."
tooltip-position="left"> tooltip-position="left">
</vn-button> </vn-button>
<a ui-sref="ticket.create" vn-bind="+"> <a ui-sref="ticket.create($ctrl.clientParams())" vn-bind="+">
<vn-button class="round md vn-mb-sm" <vn-button class="round md vn-mb-sm"
icon="add" icon="add"
vn-tooltip="New ticket" vn-tooltip="New ticket"

View File

@ -151,6 +151,14 @@ export default class Controller extends Section {
return [minHour, maxHour]; return [minHour, maxHour];
} }
clientParams() {
if (this.$params.q) {
const params = JSON.parse(this.$params.q);
if (params.clientFk) return {clientFk: params.clientFk};
}
return {};
}
} }
Controller.$inject = ['$element', '$scope', 'vnReport']; Controller.$inject = ['$element', '$scope', 'vnReport'];

View File

@ -78,4 +78,6 @@ Sale checked: Control clientes
Components: Componentes Components: Componentes
Sale tracking: Líneas preparadas Sale tracking: Líneas preparadas
Pictures: Fotos Pictures: Fotos
Log: Historial Log: Historial
Packager: Encajador
Palletizer: Palletizador

View File

@ -16,10 +16,11 @@ class Controller extends Summary {
get formattedAddress() { get formattedAddress() {
if (!this.summary) return ''; if (!this.summary) return '';
let address = this.summary.address; const address = this.summary.address;
let province = address.province ? `(${address.province.name})` : ''; const postcode = address.postalCode;
const province = address.province ? `(${address.province.name})` : '';
return `${address.street} - ${address.city} ${province}`; return `${address.street} - ${postcode} - ${address.city} ${province}`;
} }
loadData() { loadData() {

View File

@ -27,18 +27,19 @@ describe('Ticket', () => {
}); });
describe('formattedAddress()', () => { describe('formattedAddress()', () => {
it('should return a full fromatted address with city and province', () => { it('should return the full fromatted address with city and province', () => {
controller.summary = { controller.summary = {
address: { address: {
province: { province: {
name: 'Gotham' name: 'Gotham'
}, },
street: '1007 Mountain Drive', street: '1007 Mountain Drive',
postalCode: 46060,
city: 'Gotham' city: 'Gotham'
} }
}; };
expect(controller.formattedAddress).toEqual('1007 Mountain Drive - Gotham (Gotham)'); expect(controller.formattedAddress).toEqual('1007 Mountain Drive - 46060 - Gotham (Gotham)');
}); });
}); });
}); });

View File

@ -8,18 +8,18 @@
}, },
"properties": { "properties": {
"id": { "id": {
"type": "Number", "type": "number",
"id": true, "id": true,
"description": "Identifier" "description": "Identifier"
}, },
"code": { "code": {
"type": "String" "type": "string"
}, },
"name": { "name": {
"type": "String" "type": "string"
}, },
"ratio": { "ratio": {
"type": "Number" "type": "number"
} }
}, },
"acls": [ "acls": [

View File

@ -2,32 +2,24 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('absences', { Self.remoteMethodCtx('absences', {
description: 'Returns an array of absences from an specified worker', description: 'Returns an array of absences from an specified contract',
accepts: [{ accepts: [{
arg: 'workerFk', arg: 'businessFk',
type: 'Number', type: 'number',
required: true, required: true,
}, },
{ {
arg: 'started', arg: 'year',
type: 'Date', type: 'date',
required: true,
},
{
arg: 'ended',
type: 'Date',
required: true, required: true,
}], }],
returns: [{ returns: [{
arg: 'calendar'
},
{
arg: 'absences', arg: 'absences',
type: 'Number' type: 'number'
}, },
{ {
arg: 'holidays', arg: 'holidays',
type: 'Number' type: 'number'
}], }],
http: { http: {
path: `/absences`, path: `/absences`,
@ -35,25 +27,42 @@ module.exports = Self => {
} }
}); });
Self.absences = async(ctx, workerFk, yearStarted, yearEnded) => { Self.absences = async(ctx, businessFk, year, options) => {
const models = Self.app.models; const models = Self.app.models;
const isSubordinate = await models.Worker.isSubordinate(ctx, workerFk);
if (!isSubordinate) const started = new Date();
throw new UserError(`You don't have enough privileges`); started.setFullYear(year);
started.setMonth(0);
started.setDate(1);
const calendar = {totalHolidays: 0, holidaysEnjoyed: 0}; const ended = new Date();
const holidays = []; ended.setFullYear(year);
ended.setMonth(12);
ended.setDate(0);
// Get active contracts on current year let myOptions = {};
const year = yearStarted.getFullYear();
const contracts = await models.WorkerLabour.find({ if (typeof options == 'object')
Object.assign(myOptions, options);
const contract = await models.WorkerLabour.findOne({
include: [{ include: [{
relation: 'holidays', relation: 'holidays',
scope: { scope: {
where: {year} where: {year}
} }
}, },
{
relation: 'absences',
scope: {
include: {
relation: 'absenceType',
},
where: {
dated: {between: [started, ended]}
}
}
},
{ {
relation: 'workCenter', relation: 'workCenter',
scope: { scope: {
@ -67,104 +76,39 @@ module.exports = Self => {
relation: 'type' relation: 'type'
}], }],
where: { where: {
dated: {between: [yearStarted, yearEnded]} dated: {between: [started, ended]}
} }
} }
} }
} }
}], }],
where: { where: {businessFk}
and: [ }, myOptions);
{workerFk: workerFk},
{or: [{
ended: {gte: [yearStarted]}
}, {ended: null}]}
],
} if (!contract) return;
});
// Contracts ids const isSubordinate = await models.Worker.isSubordinate(ctx, contract.workerFk, myOptions);
const contractsId = contracts.map(contract => { if (!isSubordinate)
return contract.businessFk; throw new UserError(`You don't have enough privileges`);
});
// Get absences of year
let absences = await Self.find({
include: {
relation: 'absenceType'
},
where: {
businessFk: {inq: contractsId},
dated: {between: [yearStarted, yearEnded]}
}
});
let entitlementRate = 0;
absences.forEach(absence => {
const absenceType = absence.absenceType();
const isHoliday = absenceType.code === 'holiday';
const isHalfHoliday = absenceType.code === 'halfHoliday';
if (isHoliday)
calendar.holidaysEnjoyed += 1;
if (isHalfHoliday)
calendar.holidaysEnjoyed += 0.5;
entitlementRate += absenceType.holidayEntitlementRate;
const absences = [];
for (let absence of contract.absences()) {
absence.dated = new Date(absence.dated); absence.dated = new Date(absence.dated);
absence.dated.setHours(0, 0, 0, 0); absence.dated.setHours(0, 0, 0, 0);
});
// Get number of worked days absences.push(absence);
let workedDays = 0;
contracts.forEach(contract => {
const started = contract.started;
const ended = contract.ended;
const startedTime = started.getTime();
const endedTime = ended && ended.getTime() || yearEnded;
const dayTimestamp = 1000 * 60 * 60 * 24;
workedDays += Math.floor((endedTime - startedTime) / dayTimestamp);
if (workedDays > daysInYear())
workedDays = daysInYear();
// Workcenter holidays
let holidayList = contract.workCenter().holidays();
for (let day of holidayList) {
day.dated = new Date(day.dated);
day.dated.setHours(0, 0, 0, 0);
holidays.push(day);
}
});
const currentContract = contracts.find(contract => {
return contract.started <= new Date()
&& (contract.ended >= new Date() || contract.ended == null);
});
if (currentContract) {
const maxHolidays = currentContract.holidays() && currentContract.holidays().days;
calendar.totalHolidays = maxHolidays;
workedDays -= entitlementRate;
if (workedDays < daysInYear())
calendar.totalHolidays = Math.round(2 * maxHolidays * (workedDays) / daysInYear()) / 2;
} }
function daysInYear() { // Workcenter holidays
const year = yearStarted.getFullYear(); const holidays = [];
const holidayList = contract.workCenter().holidays();
for (let day of holidayList) {
day.dated = new Date(day.dated);
day.dated.setHours(0, 0, 0, 0);
return isLeapYear(year) ? 366 : 365; holidays.push(day);
} }
return [calendar, absences, holidays]; return [absences, holidays];
}; };
function isLeapYear(year) {
return year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
}
}; };

View File

@ -2,78 +2,56 @@ const app = require('vn-loopback/server/server');
describe('Worker absences()', () => { describe('Worker absences()', () => {
it('should get the absence calendar for a full year contract', async() => { it('should get the absence calendar for a full year contract', async() => {
let ctx = {req: {accessToken: {userId: 106}}}; const ctx = {req: {accessToken: {userId: 106}}};
let workerFk = 106; const businessId = 106;
const started = new Date(); const now = new Date();
started.setHours(0, 0, 0, 0); const year = now.getFullYear();
started.setMonth(0);
started.setDate(1);
const monthIndex = 11; const [absences] = await app.models.Calendar.absences(ctx, businessId, year);
const ended = new Date();
ended.setHours(0, 0, 0, 0);
ended.setMonth(monthIndex + 1);
ended.setDate(0);
let result = await app.models.Calendar.absences(ctx, workerFk, started, ended); const firstType = absences[0].absenceType().name;
let calendar = result[0]; const sixthType = absences[5].absenceType().name;
let absences = result[1];
expect(calendar.totalHolidays).toEqual(27.5);
expect(calendar.holidaysEnjoyed).toEqual(5);
let firstType = absences[0].absenceType().name;
let sixthType = absences[5].absenceType().name;
expect(firstType).toMatch(/(Holidays|Leave of absence)/); expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/); expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
}); });
it('should get the absence calendar for a permanent contract', async() => { it('should get the absence calendar for a permanent contract', async() => {
let workerFk = 106; const businessId = 106;
let worker = await app.models.WorkerLabour.findById(workerFk); const ctx = {req: {accessToken: {userId: 9}}};
let endedDate = worker.ended;
await app.models.WorkerLabour.rawSql( const now = new Date();
`UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`, const year = now.getFullYear();
[null, worker.businessFk]
);
let ctx = {req: {accessToken: {userId: 9}}}; const tx = await app.models.Calendar.beginTransaction({});
const started = new Date(); try {
started.setHours(0, 0, 0, 0); const options = {transaction: tx};
started.setMonth(0);
started.setDate(1);
const monthIndex = 11; const worker = await app.models.WorkerLabour.findById(businessId, null, options);
const ended = new Date();
ended.setHours(0, 0, 0, 0);
ended.setMonth(monthIndex + 1);
ended.setDate(0);
let result = await app.models.Calendar.absences(ctx, workerFk, started, ended); await app.models.WorkerLabour.rawSql(
let calendar = result[0]; `UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`,
let absences = result[1]; [null, worker.businessFk], options);
expect(calendar.totalHolidays).toEqual(27.5); const [absences] = await app.models.Calendar.absences(ctx, businessId, year, options);
expect(calendar.holidaysEnjoyed).toEqual(5);
let firstType = absences[0].absenceType().name; let firstType = absences[0].absenceType().name;
let sixthType = absences[5].absenceType().name; let sixthType = absences[5].absenceType().name;
expect(firstType).toMatch(/(Holidays|Leave of absence)/); expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/); expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
await tx.rollback();
// restores the contract end date } catch (e) {
await app.models.WorkerLabour.rawSql( await tx.rollback();
`UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`, throw e;
[endedDate, worker.businessFk] }
);
}); });
it('should give the same holidays as worked days since the holidays amount matches the amount of days in a year', async() => { it('should give the same holidays as worked days since the holidays amount matches the amount of days in a year', async() => {
const businessId = 106;
const userId = 106;
const today = new Date(); const today = new Date();
// getting how many days in a year // getting how many days in a year
@ -94,70 +72,47 @@ describe('Worker absences()', () => {
const daysInYear = Math.round((endedTime - startedTime) / dayTimestamp); const daysInYear = Math.round((endedTime - startedTime) / dayTimestamp);
// sets the holidays per year to the amount of days in the current year const tx = await app.models.Calendar.beginTransaction({});
let holidaysConfig = await app.models.WorkCenterHoliday.findOne({ try {
where: { const options = {transaction: tx};
workCenterFk: 1,
year: today.getFullYear()
}});
let originalHolidaysValue = holidaysConfig.days; // sets the holidays per year to the amount of days in the current year
const holidaysConfig = await app.models.WorkCenterHoliday.findOne({
where: {
workCenterFk: 1,
year: today.getFullYear()
}
}, options);
await holidaysConfig.updateAttribute('days', daysInYear); await holidaysConfig.updateAttribute('days', daysInYear, options);
// normal test begins // normal test begins
const userId = 106; const contract = await app.models.WorkerLabour.findById(businessId, null, options);
const contract = await app.models.WorkerLabour.findById(userId);
const contractStartDate = contract.started;
const startingContract = new Date(); const startingContract = new Date();
startingContract.setHours(0, 0, 0, 0); startingContract.setHours(0, 0, 0, 0);
startingContract.setMonth(today.getMonth()); startingContract.setMonth(today.getMonth());
startingContract.setDate(1); startingContract.setDate(1);
await app.models.WorkerLabour.rawSql( await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_start = ?, date_end = ? WHERE business_id = ?`, `UPDATE postgresql.business SET date_start = ?, date_end = ? WHERE business_id = ?`,
[startingContract, yearEnd, contract.businessFk] [startingContract, yearEnd, contract.businessFk], options
); );
let ctx = {req: {accessToken: {userId: userId}}}; const ctx = {req: {accessToken: {userId: userId}}};
let result = await app.models.Calendar.absences(ctx, userId, yearStart, yearEnd); const [absences] = await app.models.Calendar.absences(ctx, businessId, currentYear);
let calendar = result[0];
let absences = result[1];
let remainingDays = 0; const firstType = absences[0].absenceType().name;
for (let i = today.getMonth(); i < 12; i++) { const sixthType = absences[5].absenceType().name;
today.setDate(1);
today.setMonth(i + 1);
today.setDate(0);
remainingDays += today.getDate(); expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
} }
expect(calendar.totalHolidays).toEqual(remainingDays);
expect(calendar.holidaysEnjoyed).toEqual(5);
let firstType = absences[0].absenceType().name;
let sixthType = absences[5].absenceType().name;
expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
// resets the holidays per year with originalHolidaysValue and the contract starting date
await app.models.WorkCenterHoliday.updateAll(
{
workCenterFk: 1,
year: today.getFullYear()
},
{
days: originalHolidaysValue
}
);
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_start = ? WHERE business_id = ?`,
[contractStartDate, contract.businessFk]
);
}); });
}); });

View File

@ -0,0 +1,47 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('activeContract', {
description: 'Returns an array of contracts from an specified worker',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/activeContract`,
verb: 'GET'
}
});
Self.activeContract = async(ctx, id) => {
const models = Self.app.models;
const isSubordinate = await models.Worker.isSubordinate(ctx, id);
if (!isSubordinate)
throw new UserError(`You don't have enough privileges`);
const now = new Date();
return models.WorkerLabour.findOne({
where: {
and: [
{workerFk: id},
{started: {lte: now}},
{
or: [
{ended: {gte: now}},
{ended: null}
]
}
]
}
});
};
};

View File

@ -0,0 +1,43 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('contracts', {
description: 'Returns an array of contracts from an specified worker',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/:id/contracts`,
verb: 'GET'
}
});
Self.contracts = async(ctx, id, filter) => {
const models = Self.app.models;
const isSubordinate = await models.Worker.isSubordinate(ctx, id);
if (!isSubordinate)
throw new UserError(`You don't have enough privileges`);
if (!filter.where) filter.where = {};
const where = filter.where;
where['workerFk'] = id;
return models.WorkerLabour.find(filter);
};
};

View File

@ -5,19 +5,24 @@ module.exports = Self => {
description: 'Creates a new worker absence', description: 'Creates a new worker absence',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',
type: 'Number', type: 'number',
description: 'The worker id', description: 'The worker id',
http: {source: 'path'} http: {source: 'path'}
}, },
{
arg: 'businessFk',
type: 'number',
required: true
},
{ {
arg: 'absenceTypeId', arg: 'absenceTypeId',
type: 'Number', type: 'number',
required: true required: true
}, },
{ {
arg: 'dated', arg: 'dated',
type: 'Date', type: 'date',
required: false required: true
}], }],
returns: { returns: {
type: 'Object', type: 'Object',
@ -29,55 +34,70 @@ module.exports = Self => {
} }
}); });
Self.createAbsence = async(ctx, id, absenceTypeId, dated) => { Self.createAbsence = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const args = ctx.args;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const isSubordinate = await models.Worker.isSubordinate(ctx, id);
const isTeamBoss = await models.Account.hasRole(userId, 'teamBoss');
if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss)) let tx;
throw new UserError(`You don't have enough privileges`); let myOptions = {};
const labour = await models.WorkerLabour.findOne({ if (typeof options == 'object')
include: {relation: 'department'}, Object.assign(myOptions, options);
where: {
and: [
{workerFk: id},
{or: [{
ended: {gte: [dated]}
}, {ended: null}]}
]
}
});
const absence = await models.Calendar.create({ if (!myOptions.transaction) {
businessFk: labour.businessFk, tx = await Self.beginTransaction({});
dayOffTypeFk: absenceTypeId, myOptions.transaction = tx;
dated: dated
});
const department = labour.department();
if (department && department.notificationEmail) {
const absenceType = await models.AbsenceType.findById(absenceTypeId);
const account = await models.Account.findById(userId);
const subordinated = await models.Account.findById(id);
const origin = ctx.req.headers.origin;
const body = $t('Created absence', {
author: account.nickname,
employee: subordinated.nickname,
absenceType: absenceType.name,
dated: formatDate(dated),
workerUrl: `${origin}/#!/worker/${id}/calendar`
});
await models.Mail.create({
subject: $t('Absence change notification on the labour calendar'),
body: body,
sender: department.notificationEmail
});
} }
return absence; try {
const isSubordinate = await models.Worker.isSubordinate(ctx, id, myOptions);
const isTeamBoss = await models.Account.hasRole(userId, 'teamBoss', myOptions);
if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss))
throw new UserError(`You don't have enough privileges`);
const labour = await models.WorkerLabour.findById(args.businessFk, {
include: {relation: 'department'}
}, myOptions);
if (args.dated < labour.started || (labour.ended != null && args.dated > labour.ended))
throw new UserError(`The contract was not active during the selected date`);
const absence = await models.Calendar.create({
businessFk: labour.businessFk,
dayOffTypeFk: args.absenceTypeId,
dated: args.dated
}, myOptions);
const department = labour.department();
if (department && department.notificationEmail) {
const absenceType = await models.AbsenceType.findById(args.absenceTypeId, null, myOptions);
const account = await models.Account.findById(userId, null, myOptions);
const subordinated = await models.Account.findById(id, null, myOptions);
const origin = ctx.req.headers.origin;
const body = $t('Created absence', {
author: account.nickname,
employee: subordinated.nickname,
absenceType: absenceType.name,
dated: formatDate(args.dated),
workerUrl: `${origin}/#!/worker/${id}/calendar`
});
await models.Mail.create({
subject: $t('Absence change notification on the labour calendar'),
body: body,
sender: department.notificationEmail
}, myOptions);
}
if (tx) await tx.commit();
return absence;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
function formatDate(date) { function formatDate(date) {

View File

@ -5,13 +5,13 @@ module.exports = Self => {
description: 'Deletes a worker absence', description: 'Deletes a worker absence',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',
type: 'Number', type: 'number',
description: 'The worker id', description: 'The worker id',
http: {source: 'path'} http: {source: 'path'}
}, },
{ {
arg: 'absenceId', arg: 'absenceId',
type: 'Number', type: 'number',
required: true required: true
}], }],
returns: 'Object', returns: 'Object',
@ -21,47 +21,67 @@ module.exports = Self => {
} }
}); });
Self.deleteAbsence = async(ctx, id, absenceId) => { Self.deleteAbsence = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const args = ctx.args;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const isSubordinate = await models.Worker.isSubordinate(ctx, id);
const isTeamBoss = await models.Account.hasRole(userId, 'teamBoss');
if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss)) let tx;
throw new UserError(`You don't have enough privileges`); let myOptions = {};
const absence = await models.Calendar.findById(absenceId, { if (typeof options == 'object')
include: { Object.assign(myOptions, options);
relation: 'labour',
scope: { if (!myOptions.transaction) {
include: {relation: 'department'} tx = await Self.beginTransaction({});
} myOptions.transaction = tx;
}
});
const result = await absence.destroy();
const labour = absence.labour();
const department = labour && labour.department();
if (department && department.notificationEmail) {
const absenceType = await models.AbsenceType.findById(absence.dayOffTypeFk);
const account = await models.Account.findById(userId);
const subordinated = await models.Account.findById(labour.workerFk);
const origin = ctx.req.headers.origin;
const body = $t('Deleted absence', {
author: account.nickname,
employee: subordinated.nickname,
absenceType: absenceType.name,
dated: formatDate(absence.dated),
workerUrl: `${origin}/#!/worker/${id}/calendar`
});
await models.Mail.create({
subject: $t('Absence change notification on the labour calendar'),
body: body,
sender: department.notificationEmail
});
} }
return result; try {
const isSubordinate = await models.Worker.isSubordinate(ctx, id, myOptions);
const isTeamBoss = await models.Account.hasRole(userId, 'teamBoss', myOptions);
if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss))
throw new UserError(`You don't have enough privileges`);
const absence = await models.Calendar.findById(args.absenceId, {
include: {
relation: 'labour',
scope: {
include: {relation: 'department'}
}
}
}, myOptions);
const result = await absence.destroy(myOptions);
const labour = absence.labour();
const department = labour && labour.department();
if (department && department.notificationEmail) {
const absenceType = await models.AbsenceType.findById(absence.dayOffTypeFk, null, myOptions);
const account = await models.Account.findById(userId, null, myOptions);
const subordinated = await models.Account.findById(labour.workerFk, null, myOptions);
const origin = ctx.req.headers.origin;
const body = $t('Deleted absence', {
author: account.nickname,
employee: subordinated.nickname,
absenceType: absenceType.name,
dated: formatDate(absence.dated),
workerUrl: `${origin}/#!/worker/${id}/calendar`
});
await models.Mail.create({
subject: $t('Absence change notification on the labour calendar'),
body: body,
sender: department.notificationEmail
}, myOptions);
}
if (tx) await tx.commit();
return result;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
function formatDate(date) { function formatDate(date) {

View File

@ -34,35 +34,45 @@ module.exports = Self => {
}); });
Self.getWorkedHours = async(id, started, ended) => { Self.getWorkedHours = async(id, started, ended) => {
const models = Self.app.models;
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const worker = await models.Worker.findById(id);
const userId = worker.userFk;
const stmts = []; const stmts = [];
const startedMinusOne = new Date(started); const startedMinusOne = new Date(started);
startedMinusOne.setDate(started.getDate() - 1);
const endedPlusOne = new Date(ended); const endedPlusOne = new Date(ended);
let worker = await Self.app.models.Worker.findById(id); endedPlusOne.setDate(ended.getDate() + 1);
let userId = worker.userFk;
stmts.push(` stmts.push(`
DROP TEMPORARY TABLE IF EXISTS DROP TEMPORARY TABLE IF EXISTS
tmp.timeControlCalculate, tmp.timeControlCalculate,
tmp.timeBusinessCalculate tmp.timeBusinessCalculate
`); `);
startedMinusOne.setDate(started.getDate() - 1);
endedPlusOne.setDate(ended.getDate() + 1);
stmts.push(new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [userId, startedMinusOne, endedPlusOne])); stmts.push(new ParameterizedSQL('CALL vn.timeControl_calculateByUser(?, ?, ?)', [userId, startedMinusOne, endedPlusOne]));
stmts.push(new ParameterizedSQL('CALL vn.timeBusiness_calculateByUser(?, ?, ?)', [userId, startedMinusOne, endedPlusOne])); stmts.push(new ParameterizedSQL('CALL vn.timeBusiness_calculateByUser(?, ?, ?)', [userId, startedMinusOne, endedPlusOne]));
let resultIndex = stmts.push(new ParameterizedSQL(`
const resultIndex = stmts.push(new ParameterizedSQL(`
SELECT tbc.dated, tbc.timeWorkSeconds expectedHours, tcc.timeWorkSeconds workedHours SELECT tbc.dated, tbc.timeWorkSeconds expectedHours, tcc.timeWorkSeconds workedHours
FROM tmp.timeBusinessCalculate tbc FROM tmp.timeBusinessCalculate tbc
LEFT JOIN tmp.timeControlCalculate tcc ON tcc.dated = tbc.dated LEFT JOIN tmp.timeControlCalculate tcc ON tcc.dated = tbc.dated
WHERE tbc.dated BETWEEN ? AND ? WHERE tbc.dated BETWEEN ? AND ?
`, [started, ended])) - 1; `, [started, ended])) - 1;
stmts.push(` stmts.push(`
DROP TEMPORARY TABLE IF EXISTS DROP TEMPORARY TABLE IF EXISTS
tmp.timeControlCalculate, tmp.timeControlCalculate,
tmp.timeBusinessCalculate tmp.timeBusinessCalculate
`); `);
let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
return result[resultIndex]; return result[resultIndex];
}; };

View File

@ -0,0 +1,176 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('holidays', {
description: 'Returns the holidays available whitin a contract or a year',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'year',
type: 'date',
required: true,
},
{
arg: 'businessFk',
type: 'number',
required: false
}],
returns: [{
type: 'object',
root: true
}],
http: {
path: `/:id/holidays`,
verb: 'GET'
}
});
Self.holidays = async(ctx, id, options) => {
const models = Self.app.models;
const args = ctx.args;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const isSubordinate = await models.Worker.isSubordinate(ctx, id, myOptions);
if (!isSubordinate)
throw new UserError(`You don't have enough privileges`);
const started = new Date();
started.setFullYear(args.year);
started.setMonth(0);
started.setDate(1);
started.setHours(0, 0, 0, 0);
const ended = new Date();
ended.setFullYear(args.year);
ended.setMonth(12);
ended.setDate(0);
ended.setHours(23, 59, 59, 59);
const filter = {
include: [{
relation: 'holidays',
scope: {
where: {year: args.year}
}
},
{
relation: 'absences',
scope: {
include: {
relation: 'absenceType',
},
where: {
dated: {between: [started, ended]}
}
}
},
{
relation: 'workCenter',
scope: {
include: {
relation: 'holidays',
scope: {
include: [{
relation: 'detail'
},
{
relation: 'type'
}],
where: {
dated: {between: [started, ended]}
}
}
}
}
}],
where: {
and: [
{workerFk: id},
{
or: [
{started: {between: [started, ended]}},
{ended: {between: [started, ended]}},
{and: [{started: {lt: started}}, {ended: {gt: ended}}]},
{and: [{started: {lt: started}}, {ended: null}]}
]
}
],
}
};
if (args.businessFk)
filter.where.and.push({businessFk: args.businessFk});
const contracts = await models.WorkerLabour.find(filter, myOptions);
let totalHolidays = 0;
let holidaysEnjoyed = 0;
for (let contract of contracts) {
const contractStarted = contract.started;
contractStarted.setHours(0, 0, 0, 0);
const contractEnded = contract.ended;
if (contractEnded)
contractEnded.setHours(23, 59, 59, 59);
let startedTime;
if (contractStarted < started)
startedTime = started.getTime();
else startedTime = contractStarted.getTime();
let endedTime;
if (!contractEnded || (contractEnded && contractEnded > ended))
endedTime = ended.getTime();
else endedTime = contractEnded.getTime();
const dayTimestamp = 1000 * 60 * 60 * 24;
// Get number of worked days between dates
let workedDays = Math.floor((endedTime - startedTime) / dayTimestamp);
workedDays += 1; // 1 day inclusion
// Calculates absences
let entitlementRate = 0;
for (let absence of contract.absences()) {
const absenceType = absence.absenceType();
const isHoliday = absenceType.code === 'holiday';
const isHalfHoliday = absenceType.code === 'halfHoliday';
if (isHoliday) holidaysEnjoyed += 1;
if (isHalfHoliday) holidaysEnjoyed += 0.5;
entitlementRate += absenceType.holidayEntitlementRate;
}
workedDays -= entitlementRate;
// Max holidays for the selected year
const maxHolidays = contract.holidays() && contract.holidays().days;
if (workedDays < daysInYear())
totalHolidays += Math.round(2 * maxHolidays * (workedDays) / daysInYear()) / 2;
else totalHolidays = maxHolidays;
}
function daysInYear() {
const year = started.getFullYear();
return isLeapYear(year) ? 366 : 365;
}
return {totalHolidays, holidaysEnjoyed};
};
function isLeapYear(year) {
return year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
}
};

View File

@ -23,16 +23,21 @@ module.exports = Self => {
} }
}); });
Self.isSubordinate = async(ctx, id) => { Self.isSubordinate = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId; const myUserId = ctx.req.accessToken.userId;
const mySubordinates = await Self.mySubordinates(ctx); let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const mySubordinates = await Self.mySubordinates(ctx, myOptions);
const isSubordinate = mySubordinates.find(subordinate => { const isSubordinate = mySubordinates.find(subordinate => {
return subordinate.workerFk == id; return subordinate.workerFk == id;
}); });
const isHr = await models.Account.hasRole(myUserId, 'hr'); const isHr = await models.Account.hasRole(myUserId, 'hr', myOptions);
if (isHr || isSubordinate) if (isHr || isSubordinate)
return true; return true;

View File

@ -20,17 +20,22 @@ module.exports = Self => {
} }
}); });
Self.mySubordinates = async ctx => { Self.mySubordinates = async(ctx, options) => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const stmts = []; const stmts = [];
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [userId])); stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [userId]));
const queryIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1; const queryIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1;
stmts.push('DROP TEMPORARY TABLE tmp.subordinate'); stmts.push('DROP TEMPORARY TABLE tmp.subordinate');
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql, myOptions);
return result[queryIndex]; return result[queryIndex];
}; };

View File

@ -5,18 +5,34 @@ describe('Worker createAbsence()', () => {
const workerId = 18; const workerId = 18;
it('should return an error for a user without enough privileges', async() => { it('should return an error for a user without enough privileges', async() => {
const ctx = {req: {accessToken: {userId: 18}}}; const ctx = {
const absenceTypeId = 1; req: {accessToken: {userId: 18}},
const dated = new Date(); args: {
businessFk: 18,
absenceTypeId: 1,
dated: new Date()
}
};
let error; const tx = await app.models.Calendar.beginTransaction({});
await app.models.Worker.createAbsence(ctx, workerId, absenceTypeId, dated).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined(); try {
const options = {transaction: tx};
let error;
await app.models.Worker.createAbsence(ctx, workerId, options).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should create a new absence', async() => { it('should create a new absence', async() => {
@ -24,7 +40,14 @@ describe('Worker createAbsence()', () => {
accessToken: {userId: 19}, accessToken: {userId: 19},
headers: {origin: 'http://localhost'} headers: {origin: 'http://localhost'}
}; };
const ctx = {req: activeCtx}; const ctx = {
req: activeCtx,
args: {
businessFk: 18,
absenceTypeId: 1,
dated: new Date()
}
};
ctx.req.__ = value => { ctx.req.__ = value => {
return value; return value;
}; };
@ -32,17 +55,23 @@ describe('Worker createAbsence()', () => {
active: activeCtx active: activeCtx
}); });
const absenceTypeId = 1; const tx = await app.models.Calendar.beginTransaction({});
const dated = new Date();
const createdAbsence = await app.models.Worker.createAbsence(ctx, workerId, absenceTypeId, dated);
const expectedBusinessId = 18; try {
const expectedAbsenceTypeId = 1; const options = {transaction: tx};
expect(createdAbsence.businessFk).toEqual(expectedBusinessId); const createdAbsence = await app.models.Worker.createAbsence(ctx, workerId, options);
expect(createdAbsence.dayOffTypeFk).toEqual(expectedAbsenceTypeId);
// Restores const expectedBusinessId = 18;
await app.models.Calendar.destroyById(createdAbsence.id); const expectedAbsenceTypeId = 1;
expect(createdAbsence.businessFk).toEqual(expectedBusinessId);
expect(createdAbsence.dayOffTypeFk).toEqual(expectedAbsenceTypeId);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -12,45 +12,68 @@ describe('Worker deleteAbsence()', () => {
ctx.req.__ = value => { ctx.req.__ = value => {
return value; return value;
}; };
let createdAbsence;
beforeEach(async() => { beforeEach(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
createdAbsence = await app.models.Calendar.create({
businessFk: businessId,
dayOffTypeFk: 1,
dated: new Date()
});
});
afterEach(async() => {
await app.models.Calendar.destroyById(createdAbsence.id);
}); });
it('should return an error for a user without enough privileges', async() => { it('should return an error for a user without enough privileges', async() => {
activeCtx.accessToken.userId = 106; activeCtx.accessToken.userId = 106;
const tx = await app.models.Calendar.beginTransaction({});
let error; try {
await app.models.Worker.deleteAbsence(ctx, 18, createdAbsence.id).catch(e => { const options = {transaction: tx};
error = e; const createdAbsence = await app.models.Calendar.create({
}).finally(() => { businessFk: businessId,
expect(error.message).toEqual(`You don't have enough privileges`); dayOffTypeFk: 1,
}); dated: new Date()
}, options);
expect(error).toBeDefined(); ctx.args = {absenceId: createdAbsence.id};
let error;
await app.models.Worker.deleteAbsence(ctx, workerId).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should create a new absence', async() => { it('should successfully delete an absence', async() => {
activeCtx.accessToken.userId = 19; activeCtx.accessToken.userId = 19;
expect(createdAbsence.businessFk).toEqual(businessId); const tx = await app.models.Calendar.beginTransaction({});
await app.models.Worker.deleteAbsence(ctx, workerId, createdAbsence.id); try {
const options = {transaction: tx};
const createdAbsence = await app.models.Calendar.create({
businessFk: businessId,
dayOffTypeFk: 1,
dated: new Date()
}, options);
const deletedAbsence = await app.models.Calendar.findById(createdAbsence.id); ctx.args = {absenceId: createdAbsence.id};
expect(deletedAbsence).toBeNull(); await app.models.Worker.deleteAbsence(ctx, workerId, options);
const deletedAbsence = await app.models.Calendar.findById(createdAbsence.id, null, options);
expect(deletedAbsence).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -0,0 +1,30 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('Worker holidays()', () => {
const businessId = 106;
const workerId = 106;
const activeCtx = {
accessToken: {userId: workerId},
headers: {origin: 'http://localhost'}
};
const ctx = {req: activeCtx};
beforeEach(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should get the absence calendar for a full year contract', async() => {
const now = new Date();
const year = now.getFullYear();
ctx.args = {businessFk: businessId, year: year};
const result = await app.models.Worker.holidays(ctx, workerId);
expect(result.totalHolidays).toEqual(27.5);
expect(result.holidaysEnjoyed).toEqual(5);
});
});

View File

@ -12,10 +12,10 @@
"type": "Number" "type": "Number"
}, },
"started": { "started": {
"type": "Date" "type": "date"
}, },
"ended": { "ended": {
"type": "Date" "type": "date"
} }
}, },
"relations": { "relations": {
@ -38,6 +38,11 @@
"type": "belongsTo", "type": "belongsTo",
"model": "WorkCenterHoliday", "model": "WorkCenterHoliday",
"foreignKey": "workCenterFk" "foreignKey": "workCenterFk"
},
"absences": {
"type": "hasMany",
"model": "Calendar",
"foreignKey": "businessFk"
} }
} }
} }

View File

@ -10,4 +10,7 @@ module.exports = Self => {
require('../methods/worker/active')(Self); require('../methods/worker/active')(Self);
require('../methods/worker/activeWithRole')(Self); require('../methods/worker/activeWithRole')(Self);
require('../methods/worker/activeWithInheritedRole')(Self); require('../methods/worker/activeWithInheritedRole')(Self);
require('../methods/worker/contracts')(Self);
require('../methods/worker/holidays')(Self);
require('../methods/worker/activeContract')(Self);
}; };

View File

@ -22,13 +22,22 @@
</div> </div>
<vn-side-menu side="right"> <vn-side-menu side="right">
<div class="vn-pa-md"> <div class="vn-pa-md">
<div class="totalBox" style="text-align: center;"> <div class="totalBox vn-mb-sm" style="text-align: center;">
<h6 translate>Holidays</h6> <h6>{{'Contract' | translate}} ID: {{$ctrl.businessId}}</h6>
<div> <div>
{{'Used' | translate}} {{$ctrl.calendar.holidaysEnjoyed}} {{'Used' | translate}} {{$ctrl.contractHolidays.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.calendar.totalHolidays || 0}} {{'days' | translate}} {{'of' | translate}} {{$ctrl.contractHolidays.totalHolidays || 0}} {{'days' | translate}}
</div> </div>
</div> </div>
<div class="totalBox" style="text-align: center;">
<h6>{{'Year' | translate}} {{$ctrl.year}}</h6>
<div>
{{'Used' | translate}} {{$ctrl.yearHolidays.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.yearHolidays.totalHolidays || 0}} {{'days' | translate}}
</div>
</div>
<div class="vn-pt-md"> <div class="vn-pt-md">
<vn-autocomplete label="Year" <vn-autocomplete label="Year"
data="$ctrl.yearFilter" data="$ctrl.yearFilter"
@ -37,8 +46,23 @@
value-field="year" value-field="year"
order="DESC"> order="DESC">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete label="Contract"
url="Workers/{{$ctrl.$params.id}}/contracts"
fields="['started', 'ended']"
ng-model="$ctrl.businessId"
search-function="{businessFk: $search}"
value-field="businessFk"
order="businessFk DESC"
limit="5">
<tpl-item>
<div>ID: {{businessFk}}</div>
<div class="text-caption text-secondary">
{{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}}
</div>
</tpl-item>
</vn-autocomplete>
</div> </div>
<div class="input vn-py-md" style="overflow: hidden;"> <div name="absenceTypes" class="input vn-py-md" style="overflow: hidden;">
<vn-chip ng-repeat="absenceType in absenceTypes" ng-class="::{'selectable': $ctrl.isSubordinate}" <vn-chip ng-repeat="absenceType in absenceTypes" ng-class="::{'selectable': $ctrl.isSubordinate}"
ng-click="$ctrl.pick(absenceType)"> ng-click="$ctrl.pick(absenceType)">
<vn-avatar <vn-avatar

View File

@ -20,7 +20,24 @@ class Controller extends Section {
this.date = newYear; this.date = newYear;
this.refresh().then(() => this.repaint()); this.refresh()
.then(() => this.repaint())
.then(() => this.getContractHolidays())
.then(() => this.getYearHolidays());
}
get businessId() {
return this._businessId;
}
set businessId(value) {
this._businessId = value;
if (value) {
this.refresh()
.then(() => this.repaint())
.then(() => this.getContractHolidays())
.then(() => this.getYearHolidays());
}
} }
get date() { get date() {
@ -31,16 +48,6 @@ class Controller extends Section {
this._date = value; this._date = value;
value.setHours(0, 0, 0, 0); value.setHours(0, 0, 0, 0);
const started = new Date(value.getTime());
started.setMonth(0);
started.setDate(1);
this.started = started;
const ended = new Date(value.getTime());
ended.setMonth(12);
ended.setDate(0);
this.ended = ended;
this.months = new Array(12); this.months = new Array(12);
for (let i = 0; i < this.months.length; i++) { for (let i = 0; i < this.months.length; i++) {
@ -59,26 +66,51 @@ class Controller extends Section {
this._worker = value; this._worker = value;
if (value) { if (value) {
this.refresh().then(() => this.repaint());
this.getIsSubordinate(); this.getIsSubordinate();
this.getActiveContract();
} }
} }
buildYearFilter() { buildYearFilter() {
const currentYear = new Date().getFullYear(); const now = new Date();
const minRange = currentYear - 5; now.setFullYear(now.getFullYear() + 1);
const maxYear = now.getFullYear();
const minRange = maxYear - 5;
const years = []; const years = [];
for (let i = currentYear; i > minRange; i--) for (let i = maxYear; i > minRange; i--)
years.push({year: i}); years.push({year: i});
this.yearFilter = years; this.yearFilter = years;
} }
getIsSubordinate() { getIsSubordinate() {
this.$http.get(`Workers/${this.worker.id}/isSubordinate`).then(res => this.$http.get(`Workers/${this.worker.id}/isSubordinate`)
this.isSubordinate = res.data .then(res => this.isSubordinate = res.data);
); }
getActiveContract() {
this.$http.get(`Workers/${this.worker.id}/activeContract`)
.then(res => this.businessId = res.data.businessFk);
}
getContractHolidays() {
this.getHolidays({
businessFk: this.businessId,
year: this.year
}, data => this.contractHolidays = data);
}
getYearHolidays() {
this.getHolidays({
year: this.year
}, data => this.yearHolidays = data);
}
getHolidays(params, cb) {
this.$http.get(`Workers/${this.worker.id}/holidays`, {params})
.then(res => cb(res.data));
} }
onData(data) { onData(data) {
@ -155,9 +187,6 @@ class Controller extends Section {
if (!this.absenceType) if (!this.absenceType)
return this.vnApp.showMessage(this.$t('Choose an absence type from the right menu')); return this.vnApp.showMessage(this.$t('Choose an absence type from the right menu'));
if (this.year != new Date().getFullYear())
return this.vnApp.showMessage(this.$t('You can just add absences within the current year'));
const day = $days[0]; const day = $days[0];
const stamp = day.getTime(); const stamp = day.getTime();
const event = this.events[stamp]; const event = this.events[stamp];
@ -176,7 +205,8 @@ class Controller extends Section {
const absenceType = this.absenceType; const absenceType = this.absenceType;
const params = { const params = {
dated: dated, dated: dated,
absenceTypeId: absenceType.id absenceTypeId: absenceType.id,
businessFk: this.businessId
}; };
const path = `Workers/${this.$params.id}/createAbsence`; const path = `Workers/${this.$params.id}/createAbsence`;
@ -190,7 +220,10 @@ class Controller extends Section {
}; };
this.repaintCanceller(() => this.repaintCanceller(() =>
this.refresh().then(calendar.repaint()) this.refresh()
.then(calendar.repaint())
.then(() => this.getContractHolidays())
.then(() => this.getYearHolidays())
); );
}); });
} }
@ -208,7 +241,10 @@ class Controller extends Section {
event.type = absenceType.code; event.type = absenceType.code;
this.repaintCanceller(() => this.repaintCanceller(() =>
this.refresh().then(calendar.repaint()) this.refresh()
.then(calendar.repaint())
.then(() => this.getContractHolidays())
.then(() => this.getYearHolidays())
); );
}); });
} }
@ -220,7 +256,10 @@ class Controller extends Section {
delete this.events[day.getTime()]; delete this.events[day.getTime()];
this.repaintCanceller(() => this.repaintCanceller(() =>
this.refresh().then(calendar.repaint()) this.refresh()
.then(calendar.repaint())
.then(() => this.getContractHolidays())
.then(() => this.getYearHolidays())
); );
}); });
} }
@ -237,9 +276,8 @@ class Controller extends Section {
refresh() { refresh() {
const params = { const params = {
workerFk: this.worker.id, businessFk: this.businessId,
started: this.started, year: this.year
ended: this.ended
}; };
return this.$http.get(`Calendars/absences`, {params}) return this.$http.get(`Calendars/absences`, {params})
.then(res => this.onData(res.data)); .then(res => this.onData(res.data));

View File

@ -41,35 +41,31 @@ describe('Worker', () => {
}); });
}); });
describe('started property', () => { describe('businessId() setter', () => {
it(`should return first day and month of current year`, () => { it(`should set the contract id and then call to the refresh method`, () => {
let started = new Date(year, 0, 1); jest.spyOn(controller, 'refresh').mockReturnValue(Promise.resolve());
expect(controller.started).toEqual(started); controller.businessId = 106;
});
});
describe('ended property', () => { expect(controller.refresh).toHaveBeenCalledWith();
it(`should return last day and month of current year`, () => {
let ended = new Date(year, 11, 31);
expect(controller.ended).toEqual(ended);
}); });
}); });
describe('months property', () => { describe('months property', () => {
it(`should return an array of twelve months length`, () => { it(`should return an array of twelve months length`, () => {
const started = new Date(year, 0, 1);
const ended = new Date(year, 11, 1); const ended = new Date(year, 11, 1);
expect(controller.months.length).toEqual(12); expect(controller.months.length).toEqual(12);
expect(controller.months[0]).toEqual(controller.started); expect(controller.months[0]).toEqual(started);
expect(controller.months[11]).toEqual(ended); expect(controller.months[11]).toEqual(ended);
}); });
}); });
describe('worker() setter', () => { describe('worker() setter', () => {
it(`should perform a get query and set the reponse data on the model`, () => { it(`should perform a get query and set the reponse data on the model`, () => {
jest.spyOn(controller, 'getIsSubordinate').mockReturnValue(true); controller.getIsSubordinate = jest.fn();
controller.getActiveContract = jest.fn();
let today = new Date(); let today = new Date();
let tomorrow = new Date(today.getTime()); let tomorrow = new Date(today.getTime());
@ -78,49 +74,60 @@ describe('Worker', () => {
let yesterday = new Date(today.getTime()); let yesterday = new Date(today.getTime());
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
$httpBackend.whenRoute('GET', 'Calendars/absences')
.respond({
holidays: [
{dated: today, detail: {name: 'New year'}},
{dated: tomorrow, detail: {name: 'Easter'}}
],
absences: [
{dated: today, absenceType: {name: 'Holiday', rgb: '#aaa'}},
{dated: yesterday, absenceType: {name: 'Leave', rgb: '#bbb'}}
]
});
controller.worker = {id: 107}; controller.worker = {id: 107};
expect(controller.getIsSubordinate).toHaveBeenCalledWith();
expect(controller.getActiveContract).toHaveBeenCalledWith();
});
});
describe('getIsSubordinate()', () => {
it(`should return whether the worker is a subordinate`, () => {
$httpBackend.expect('GET', `Workers/106/isSubordinate`).respond(true);
controller.getIsSubordinate();
$httpBackend.flush(); $httpBackend.flush();
let events = controller.events; expect(controller.isSubordinate).toBe(true);
});
});
expect(events[today.getTime()].name).toEqual('New year, Holiday'); describe('getActiveContract()', () => {
expect(events[tomorrow.getTime()].name).toEqual('Easter'); it(`should return the current contract and then set the businessId property`, () => {
expect(events[yesterday.getTime()].name).toEqual('Leave'); jest.spyOn(controller, 'refresh').mockReturnValue(Promise.resolve());
expect(events[yesterday.getTime()].color).toEqual('#bbb');
expect(controller.getIsSubordinate).toHaveBeenCalledWith(); $httpBackend.expect('GET', `Workers/106/activeContract`).respond({businessFk: 106});
controller.getActiveContract();
$httpBackend.flush();
expect(controller.businessId).toEqual(106);
});
});
describe('getContractHolidays()', () => {
it(`should return the worker holidays amount and then set the contractHolidays property`, () => {
const today = new Date();
const year = today.getFullYear();
const serializedParams = $httpParamSerializer({year});
$httpBackend.expect('GET', `Workers/106/holidays?${serializedParams}`).respond({totalHolidays: 28});
controller.getContractHolidays();
$httpBackend.flush();
expect(controller.contractHolidays).toEqual({totalHolidays: 28});
}); });
}); });
describe('formatDay()', () => { describe('formatDay()', () => {
it(`should set the day element style`, () => { it(`should set the day element style`, () => {
jest.spyOn(controller, 'getIsSubordinate').mockReturnThis(); const today = new Date();
let today = new Date(); controller.events[today.getTime()] = {
name: 'Holiday',
color: '#000'
};
$httpBackend.whenRoute('GET', 'Calendars/absences') const dayElement = angular.element('<div><section></section></div>')[0];
.respond({ const dayNumber = dayElement.firstElementChild;
absences: [
{dated: today, absenceType: {name: 'Holiday', rgb: '#000'}}
]
});
controller.worker = {id: 1};
$httpBackend.flush();
let dayElement = angular.element('<div><section></section></div>')[0];
let dayNumber = dayElement.firstElementChild;
controller.formatDay(today, dayElement); controller.formatDay(today, dayElement);
@ -160,28 +167,6 @@ describe('Worker', () => {
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Choose an absence type from the right menu'); expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Choose an absence type from the right menu');
}); });
it(`should show an snackbar message if the selected day is not within the current year`, () => {
jest.spyOn(controller.vnApp, 'showMessage').mockReturnThis();
const selectedDay = new Date();
const $event = {
target: {
closest: () => {
return {$ctrl: {}};
}
}
};
const $days = [selectedDay];
const pastYear = new Date();
pastYear.setFullYear(pastYear.getFullYear() - 1);
controller.date = pastYear;
controller.absenceType = {id: 1};
controller.onSelection($event, $days);
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('You can just add absences within the current year');
});
it(`should call to the create() method`, () => { it(`should call to the create() method`, () => {
jest.spyOn(controller, 'create').mockReturnThis(); jest.spyOn(controller, 'create').mockReturnThis();
@ -342,20 +327,8 @@ describe('Worker', () => {
it(`should make a HTTP GET query and then call to the onData() method`, () => { it(`should make a HTTP GET query and then call to the onData() method`, () => {
jest.spyOn(controller, 'onData').mockReturnThis(); jest.spyOn(controller, 'onData').mockReturnThis();
const dated = controller.date;
const started = new Date(dated.getTime());
started.setMonth(0);
started.setDate(1);
const ended = new Date(dated.getTime());
ended.setMonth(12);
ended.setDate(0);
controller.started = started;
controller.ended = ended;
const expecteResponse = [{id: 1}]; const expecteResponse = [{id: 1}];
const expectedParams = {workerFk: 106, started: started, ended: ended}; const expectedParams = {year: year};
const serializedParams = $httpParamSerializer(expectedParams); const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expect('GET', `Calendars/absences?${serializedParams}`).respond(200, expecteResponse); $httpBackend.expect('GET', `Calendars/absences?${serializedParams}`).respond(200, expecteResponse);
controller.refresh(); controller.refresh();

View File

@ -1,5 +1,5 @@
Calendar: Calendario Calendar: Calendario
Holidays: Vacaciones Contract: Contrato
Festive: Festivo Festive: Festivo
Used: Utilizados Used: Utilizados
Year: Año Year: Año

View File

@ -13,6 +13,19 @@ class Controller extends Section {
this.date = new Date(); this.date = new Date();
} }
get worker() {
return this._worker;
}
set worker(value) {
this._worker = value;
if (value) {
this.getActiveContract()
.then(() => this.getAbsences());
}
}
/** /**
* The current selected date * The current selected date
*/ */
@ -70,6 +83,11 @@ class Controller extends Section {
} }
} }
getActiveContract() {
return this.$http.get(`Workers/${this.worker.id}/activeContract`)
.then(res => this.businessId = res.data.businessFk);
}
fetchHours() { fetchHours() {
const params = {workerFk: this.$params.id}; const params = {workerFk: this.$params.id};
const filter = { const filter = {
@ -80,7 +98,6 @@ class Controller extends Section {
}; };
this.$.model.applyFilter(filter, params).then(() => { this.$.model.applyFilter(filter, params).then(() => {
this.getWorkedHours(this.started, this.ended); this.getWorkedHours(this.started, this.ended);
this.getAbsences();
}); });
} }
@ -89,10 +106,10 @@ class Controller extends Section {
} }
getAbsences() { getAbsences() {
const fullYear = this.started.getFullYear();
let params = { let params = {
workerFk: this.$params.id, businessFk: this.businessId,
started: this.started, year: fullYear
ended: this.ended
}; };
return this.$http.get(`Calendars/absences`, {params}) return this.$http.get(`Calendars/absences`, {params})
@ -257,5 +274,8 @@ Controller.$inject = ['$element', '$scope', 'vnWeekDays'];
ngModule.vnComponent('vnWorkerTimeControl', { ngModule.vnComponent('vnWorkerTimeControl', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller controller: Controller,
bindings: {
worker: '<'
}
}); });