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),
(2, 1, 2),
(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"]',
advancedSearchClient: 'vn-ticket-search-panel vn-textfield[ng-model="filter.clientFk"]',
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',
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',
@ -888,7 +888,7 @@ export default {
},
workerCalendar: {
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',
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',
@ -896,11 +896,11 @@ export default {
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',
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)',
absence: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(2)',
halfHoliday: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(3)',
furlough: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(4)',
halfFurlough: 'vn-worker-calendar > vn-side-menu div:nth-child(3) > vn-chip:nth-child(5)',
holidays: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(1)',
absence: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(2)',
halfHoliday: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(3)',
furlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(4)',
halfFurlough: 'vn-worker-calendar > vn-side-menu [name="absenceTypes"] > vn-chip:nth-child(5)',
},
invoiceOutIndex: {
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() => {
await page.write(selectors.travelIndex.reference, 'Testing reference');
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);
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() => {
// 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');
});

View File

@ -11,7 +11,7 @@ import './style.scss';
* @property {String} valueField The data field name that should be used as value
* @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 {Boolean} multiple Wether to allow multiple selection
* @property {Boolean} multiple Whether to allow multiple selection
* @property {Object} selection Current object selected
*
* @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() {
return this._shown;

View File

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

View File

@ -180,5 +180,6 @@
"This genus already exist": "Este genus 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}}})",
"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);
let filter = {
const filter = {
fields: [
'id',
'itemFk',
@ -136,7 +136,7 @@ module.exports = Self => {
}
};
let stmts = [];
const stmts = [];
let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.buyRecalc');

View File

@ -32,7 +32,7 @@ module.exports = Self => {
}
try {
let promises = [];
const promises = [];
for (let buy of ctx.args.buys) {
const buysToDelete = models.Buy.destroyById(buy.id, myOptions);
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 identifier;
@ -60,28 +71,27 @@ module.exports = Self => {
const models = Self.app.models;
const model = models[modelName];
let tx = await model.beginTransaction({});
try {
let promises = [];
let options = {transaction: tx};
let targets = lines.map(line => {
const targets = lines.map(line => {
return line[identifier];
});
let value = {};
const value = {};
value[field] = newValue;
// intentarlo con updateAll
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);
await tx.commit();
} catch (error) {
await tx.rollback();
throw error;
const result = await Promise.all(promises, myOptions);
if (tx) await tx.commit();
return result;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

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

View File

@ -1,14 +1,22 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('getBuys', {
description: 'Returns buys for one entry',
accessType: 'READ',
accepts: {
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The entry 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
@ -19,8 +27,14 @@ module.exports = Self => {
}
});
Self.getBuys = async id => {
let filter = {
Self.getBuys = async(id, filter, options) => {
const models = Self.app.models;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
let defaultFilter = {
where: {entryFk: id},
fields: [
'id',
@ -68,7 +82,8 @@ module.exports = Self => {
}
};
let buys = await Self.app.models.Buy.find(filter);
return buys;
defaultFilter = mergeFilters(defaultFilter, filter);
return models.Buy.find(defaultFilter, myOptions);
};
};

View File

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

View File

@ -45,8 +45,8 @@ module.exports = Self => {
const conn = Self.dataSource.connector;
const args = ctx.args;
const models = Self.app.models;
let tx;
if (!options.transaction) {
tx = await Self.beginTransaction({});
options.transaction = tx;
@ -76,7 +76,7 @@ module.exports = Self => {
const createdBuys = await models.Buy.create(buys, options);
const buyIds = createdBuys.map(buy => buy.id);
let stmts = [];
const stmts = [];
let stmt;
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;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
for (let buy of buys) {
const packaging = await models.Packaging.findOne({
fields: ['id'],
where: {volume: {gte: buy.volume}},
order: 'volume ASC'
});
}, myOptions);
buy.packageFk = packaging.id;
}

View File

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

View File

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

View File

@ -10,7 +10,7 @@ Commission: Comisión
Landed: F. entrega
Reference: Referencia
Created: Creado
Booked: Facturado
Booked: Contabilizada
Is inventory: Inventario
Notes: Notas
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">
<h5>
<a ng-if="::$ctrl.entryData.id"
@ -95,7 +102,7 @@
<th translate center expand field="price3">Packing price</th>
</tr>
</thead>
<tbody ng-repeat="line in $ctrl.buys">
<tbody ng-repeat="line in buys">
<tr>
<td center title="{{::line.quantity}}">{{::line.quantity}}</td>
<td center title="{{::line.stickers | dashIfEmpty}}">{{::line.stickers | dashIfEmpty}}</td>
@ -156,6 +163,10 @@
</tr>
</tbody>
</table>
<vn-pagination
model="buysModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto>
</vn-horizontal>
</vn-card>

View File

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

View File

@ -46,20 +46,4 @@ describe('component vnEntrySummary', () => {
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.docFk AS dmsFk,
ii.supplierFk,
ii.expenceFkDeductible deductibleExpenseFk,
s.name AS supplierName,
s.account,
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": {
"type": "boolean"
},
"isVatDeductible": {
"type": "boolean"
},
"booked": {
"type": "date"
},
@ -50,6 +47,12 @@
"mysql": {
"columnName": "docFk"
}
},
"deductibleExpenseFk": {
"type": "number",
"mysql": {
"columnName": "expenceFkDeductible"
}
}
},
"relations": {
@ -68,6 +71,11 @@
"model": "SageWithholding",
"foreignKey": "withholdingSageFk"
},
"expenseDeductible": {
"type": "belongsTo",
"model": "Expense",
"foreignKey": "deductibleExpenseFk"
},
"company": {
"type": "belongsTo",
"model": "Company",

View File

@ -7,21 +7,6 @@
</vn-watcher>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<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-autocomplete
vn-one
@ -44,6 +29,35 @@
rule>
</vn-textfield>
</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-date-picker
vn-one

View File

@ -18,6 +18,8 @@
</vn-label-value>
<vn-label-value label="Supplier ref" value="{{$ctrl.summary.supplierRef}}">
</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>
</vn-one>
@ -34,11 +36,11 @@
<vn-one>
<vn-label-value label="Sage withholding" value="{{$ctrl.summary.sageWithholding.withholding}}">
</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>
<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>
</vn-vertical>

View File

@ -7,4 +7,4 @@ Booked date: Fecha contable
Accounted date: Fecha contable
Doc number: Numero documento
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);
});
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() => {
const summary = await app.models.InvoiceOut.summary(1);
const supplier = summary.invoiceOut.supplier();

View File

@ -1,5 +1,3 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('summary', {
description: 'The invoiceOut summary',
@ -22,7 +20,6 @@ module.exports = Self => {
});
Self.summary = async id => {
const conn = Self.dataSource.connector;
let summary = {};
const filter = {
@ -57,54 +54,20 @@ module.exports = Self => {
scope: {
fields: ['id', 'socialName']
}
},
{
relation: 'tickets'
}
]
};
summary.invoiceOut = await Self.app.models.InvoiceOut.findOne(filter);
let stmts = [];
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(`
const invoiceOutTaxes = await Self.rawSql(`
SELECT iot.* , pgc.*, IF(pe.equFk IS NULL, taxableBase, 0) AS Base, pgc.rate / 100 as vatPercent
FROM vn.invoiceOutTax iot
JOIN vn.pgc ON pgc.code = iot.pgcFk
LEFT JOIN vn.pgcEqu pe ON pe.equFk = pgc.code
WHERE invoiceOutFk = ?`, [summary.invoiceOut.id]);
let invoiceOutTaxesIndex = stmts.push(stmt) - 1;
stmts.push(
`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];
summary.invoiceOut.taxesBreakdown = invoiceOutTaxes;
return summary;
};

View File

@ -1,6 +1,7 @@
module.exports = Self => {
require('../methods/invoiceOut/filter')(Self);
require('../methods/invoiceOut/summary')(Self);
require('../methods/invoiceOut/getTickets')(Self);
require('../methods/invoiceOut/download')(Self);
require('../methods/invoiceOut/delete')(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">
<h5>
<a ng-if="::$ctrl.summary.invoiceOut.id"
@ -59,7 +66,7 @@
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="ticket in $ctrl.summary.invoiceOut.tickets">
<vn-tr ng-repeat="ticket in tickets">
<vn-td number>
<span
ng-click="ticketDescriptor.show($event, ticket.id)"
@ -75,10 +82,14 @@
</span>
</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-tbody>
</vn-table>
<vn-pagination
model="ticketsModel"
class="vn-pt-xs">
</vn-pagination>
</vn-auto>
</vn-horizontal>
</vn-card>

View File

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

View File

@ -3,6 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('route updateVolume()', () => {
const routeId = 1;
const ticketId = 14;
const userId = 50;
const activeCtx = {
accessToken: {userId: userId},
@ -13,31 +14,39 @@ describe('route updateVolume()', () => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
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);
await ticket.updateAttributes({routeFk: routeId});
await app.models.Route.updateVolume(ctx, routeId);
try {
const options = {transaction: tx};
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 => {
if (log.newInstance)
return log.newInstance.m3 === updatedRoute.m3;
});
const logIdToDestroy = m3Log[0].id;
const updatedRoute = await app.models.Route.findById(routeId, null, options);
expect(m3Log.length).toEqual(1);
expect(updatedRoute.m3).not.toEqual(route.m3);
// restores
await ticket.updateAttributes({routeFk: null});
await route.updateAttributes({m3: 1.8});
await app.models.RouteLog.destroyById(logIdToDestroy);
const logs = await app.models.RouteLog.find({
fields: ['id', 'newInstance']
}, options);
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) => {
let query = `CALL vn.routeUpdateM3(?)`;
let userId = ctx.req.accessToken.userId;
let originalRoute = await Self.app.models.Route.findById(id);
Self.updateVolume = async(ctx, id, options) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
await Self.rawSql(query, [id]);
let updatedRoute = await Self.app.models.Route.findById(id);
let tx;
let myOptions = {};
let logRecord = {
originFk: id,
userFk: userId,
action: 'update',
changedModel: 'Route',
changedModelId: id,
oldInstance: {m3: originalRoute.m3},
newInstance: {m3: updatedRoute.m3}
};
if (typeof options == 'object')
Object.assign(myOptions, options);
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">
</vn-check>
</vn-td>
<vn-td number>{{::ticket.id}}</vn-td>
<vn-td number>
<span
ng-click="::$ctrl.showClientDescriptor($event, ticket.clientFk)"
class="link">
<span class="link" ng-click="ticketDescriptor.show($event, ticket.id)">
{{::ticket.id}}
</span>
</vn-td>
<vn-td number>
<span class="link" ng-click="clientDescriptor.show($event, ticket.clientFk)">
{{::ticket.nickname}}
</span>
</vn-td>
@ -180,3 +182,9 @@
vn-bind="+"
fixed-bottom-right>
</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,
i3.name packagingName,
i3.id packagingItemFk,
e.packagingFk
e.packagingFk,
es.workerFk expeditionScanWorkerFk,
su.nickname scannerUserNickname,
es.scanned
FROM
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
LEFT JOIN vn.packaging p ON p.id = e.packagingFk
LEFT JOIN vn.item i3 ON i3.id = p.itemFk
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'));

View File

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

View File

@ -63,7 +63,7 @@ module.exports = Self => {
}, {
relation: 'address',
scope: {
fields: ['street', 'city', 'provinceFk', 'phone', 'mobile'],
fields: ['street', 'city', 'provinceFk', 'phone', 'mobile', 'postalCode'],
include: {
relation: 'province',
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 parameters: No hay una zona de reparto disponible con estos parámetros
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.agencies = res.data;
const defaultAgency = this.agencies.find(agency=> {
const defaultAgency = this.agencies.find(agency => {
return agency.agencyModeFk == this.defaultAddress.agencyModeFk;
});
if (defaultAgency)

View File

@ -19,8 +19,10 @@
<vn-th field="isBox">Package type</vn-th>
<vn-th field="counter" number>Counter</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="scanned" expand>Scanned</vn-th>
<vn-th field="expeditionScanWorkerFk">Palletizer</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
@ -51,6 +53,12 @@
</span>
</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-tbody>
</vn-table>

View File

@ -166,7 +166,7 @@
vn-tooltip="Payment on account..."
tooltip-position="left">
</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"
icon="add"
vn-tooltip="New ticket"

View File

@ -151,6 +151,14 @@ export default class Controller extends Section {
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'];

View File

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

View File

@ -16,10 +16,11 @@ class Controller extends Summary {
get formattedAddress() {
if (!this.summary) return '';
let address = this.summary.address;
let province = address.province ? `(${address.province.name})` : '';
const address = this.summary.address;
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() {

View File

@ -27,18 +27,19 @@ describe('Ticket', () => {
});
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 = {
address: {
province: {
name: 'Gotham'
},
street: '1007 Mountain Drive',
postalCode: 46060,
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": {
"id": {
"type": "Number",
"type": "number",
"id": true,
"description": "Identifier"
},
"code": {
"type": "String"
"type": "string"
},
"name": {
"type": "String"
"type": "string"
},
"ratio": {
"type": "Number"
"type": "number"
}
},
"acls": [

View File

@ -2,32 +2,24 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('absences', {
description: 'Returns an array of absences from an specified worker',
description: 'Returns an array of absences from an specified contract',
accepts: [{
arg: 'workerFk',
type: 'Number',
arg: 'businessFk',
type: 'number',
required: true,
},
{
arg: 'started',
type: 'Date',
required: true,
},
{
arg: 'ended',
type: 'Date',
arg: 'year',
type: 'date',
required: true,
}],
returns: [{
arg: 'calendar'
},
{
arg: 'absences',
type: 'Number'
type: 'number'
},
{
arg: 'holidays',
type: 'Number'
type: 'number'
}],
http: {
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 isSubordinate = await models.Worker.isSubordinate(ctx, workerFk);
if (!isSubordinate)
throw new UserError(`You don't have enough privileges`);
const started = new Date();
started.setFullYear(year);
started.setMonth(0);
started.setDate(1);
const calendar = {totalHolidays: 0, holidaysEnjoyed: 0};
const holidays = [];
const ended = new Date();
ended.setFullYear(year);
ended.setMonth(12);
ended.setDate(0);
// Get active contracts on current year
const year = yearStarted.getFullYear();
const contracts = await models.WorkerLabour.find({
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const contract = await models.WorkerLabour.findOne({
include: [{
relation: 'holidays',
scope: {
where: {year}
}
},
{
relation: 'absences',
scope: {
include: {
relation: 'absenceType',
},
where: {
dated: {between: [started, ended]}
}
}
},
{
relation: 'workCenter',
scope: {
@ -67,104 +76,39 @@ module.exports = Self => {
relation: 'type'
}],
where: {
dated: {between: [yearStarted, yearEnded]}
dated: {between: [started, ended]}
}
}
}
}
}],
where: {
and: [
{workerFk: workerFk},
{or: [{
ended: {gte: [yearStarted]}
}, {ended: null}]}
],
where: {businessFk}
}, myOptions);
}
});
if (!contract) return;
// Contracts ids
const contractsId = contracts.map(contract => {
return contract.businessFk;
});
// 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 isSubordinate = await models.Worker.isSubordinate(ctx, contract.workerFk, myOptions);
if (!isSubordinate)
throw new UserError(`You don't have enough privileges`);
const absences = [];
for (let absence of contract.absences()) {
absence.dated = new Date(absence.dated);
absence.dated.setHours(0, 0, 0, 0);
});
// Get number of worked days
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;
absences.push(absence);
}
function daysInYear() {
const year = yearStarted.getFullYear();
// Workcenter holidays
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()', () => {
it('should get the absence calendar for a full year contract', async() => {
let ctx = {req: {accessToken: {userId: 106}}};
let workerFk = 106;
const ctx = {req: {accessToken: {userId: 106}}};
const businessId = 106;
const started = new Date();
started.setHours(0, 0, 0, 0);
started.setMonth(0);
started.setDate(1);
const now = new Date();
const year = now.getFullYear();
const monthIndex = 11;
const ended = new Date();
ended.setHours(0, 0, 0, 0);
ended.setMonth(monthIndex + 1);
ended.setDate(0);
const [absences] = await app.models.Calendar.absences(ctx, businessId, year);
let result = await app.models.Calendar.absences(ctx, workerFk, started, ended);
let calendar = result[0];
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;
const firstType = absences[0].absenceType().name;
const sixthType = absences[5].absenceType().name;
expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
});
it('should get the absence calendar for a permanent contract', async() => {
let workerFk = 106;
let worker = await app.models.WorkerLabour.findById(workerFk);
let endedDate = worker.ended;
const businessId = 106;
const ctx = {req: {accessToken: {userId: 9}}};
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`,
[null, worker.businessFk]
);
const now = new Date();
const year = now.getFullYear();
let ctx = {req: {accessToken: {userId: 9}}};
const tx = await app.models.Calendar.beginTransaction({});
const started = new Date();
started.setHours(0, 0, 0, 0);
started.setMonth(0);
started.setDate(1);
try {
const options = {transaction: tx};
const monthIndex = 11;
const ended = new Date();
ended.setHours(0, 0, 0, 0);
ended.setMonth(monthIndex + 1);
ended.setDate(0);
const worker = await app.models.WorkerLabour.findById(businessId, null, options);
let result = await app.models.Calendar.absences(ctx, workerFk, started, ended);
let calendar = result[0];
let absences = result[1];
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`,
[null, worker.businessFk], options);
expect(calendar.totalHolidays).toEqual(27.5);
expect(calendar.holidaysEnjoyed).toEqual(5);
const [absences] = await app.models.Calendar.absences(ctx, businessId, year, options);
let firstType = absences[0].absenceType().name;
let sixthType = absences[5].absenceType().name;
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)/);
// restores the contract end date
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_end = ? WHERE business_id = ?`,
[endedDate, worker.businessFk]
);
expect(firstType).toMatch(/(Holidays|Leave of absence)/);
expect(sixthType).toMatch(/(Holidays|Leave of absence)/);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
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();
// getting how many days in a year
@ -94,70 +72,47 @@ describe('Worker absences()', () => {
const daysInYear = Math.round((endedTime - startedTime) / dayTimestamp);
// sets the holidays per year to the amount of days in the current year
let holidaysConfig = await app.models.WorkCenterHoliday.findOne({
where: {
workCenterFk: 1,
year: today.getFullYear()
}});
const tx = await app.models.Calendar.beginTransaction({});
try {
const options = {transaction: tx};
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
const userId = 106;
const contract = await app.models.WorkerLabour.findById(userId);
const contractStartDate = contract.started;
// normal test begins
const contract = await app.models.WorkerLabour.findById(businessId, null, options);
const startingContract = new Date();
startingContract.setHours(0, 0, 0, 0);
startingContract.setMonth(today.getMonth());
startingContract.setDate(1);
const startingContract = new Date();
startingContract.setHours(0, 0, 0, 0);
startingContract.setMonth(today.getMonth());
startingContract.setDate(1);
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_start = ?, date_end = ? WHERE business_id = ?`,
[startingContract, yearEnd, contract.businessFk]
);
await app.models.WorkerLabour.rawSql(
`UPDATE postgresql.business SET date_start = ?, date_end = ? WHERE business_id = ?`,
[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);
let calendar = result[0];
let absences = result[1];
const [absences] = await app.models.Calendar.absences(ctx, businessId, currentYear);
let remainingDays = 0;
for (let i = today.getMonth(); i < 12; i++) {
today.setDate(1);
today.setMonth(i + 1);
today.setDate(0);
const firstType = absences[0].absenceType().name;
const sixthType = absences[5].absenceType().name;
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',
accepts: [{
arg: 'id',
type: 'Number',
type: 'number',
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'businessFk',
type: 'number',
required: true
},
{
arg: 'absenceTypeId',
type: 'Number',
type: 'number',
required: true
},
{
arg: 'dated',
type: 'Date',
required: false
type: 'date',
required: true
}],
returns: {
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 $t = ctx.req.__; // $translate
const args = ctx.args;
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))
throw new UserError(`You don't have enough privileges`);
let tx;
let myOptions = {};
const labour = await models.WorkerLabour.findOne({
include: {relation: 'department'},
where: {
and: [
{workerFk: id},
{or: [{
ended: {gte: [dated]}
}, {ended: null}]}
]
}
});
if (typeof options == 'object')
Object.assign(myOptions, options);
const absence = await models.Calendar.create({
businessFk: labour.businessFk,
dayOffTypeFk: absenceTypeId,
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
});
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
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) {

View File

@ -5,13 +5,13 @@ module.exports = Self => {
description: 'Deletes a worker absence',
accepts: [{
arg: 'id',
type: 'Number',
type: 'number',
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'absenceId',
type: 'Number',
type: 'number',
required: true
}],
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 $t = ctx.req.__; // $translate
const args = ctx.args;
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))
throw new UserError(`You don't have enough privileges`);
let tx;
let myOptions = {};
const absence = await models.Calendar.findById(absenceId, {
include: {
relation: 'labour',
scope: {
include: {relation: 'department'}
}
}
});
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
});
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
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) {

View File

@ -34,35 +34,45 @@ module.exports = Self => {
});
Self.getWorkedHours = async(id, started, ended) => {
const models = Self.app.models;
const conn = Self.dataSource.connector;
const worker = await models.Worker.findById(id);
const userId = worker.userFk;
const stmts = [];
const startedMinusOne = new Date(started);
startedMinusOne.setDate(started.getDate() - 1);
const endedPlusOne = new Date(ended);
let worker = await Self.app.models.Worker.findById(id);
let userId = worker.userFk;
endedPlusOne.setDate(ended.getDate() + 1);
stmts.push(`
DROP TEMPORARY TABLE IF EXISTS
tmp.timeControlCalculate,
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.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
FROM tmp.timeBusinessCalculate tbc
LEFT JOIN tmp.timeControlCalculate tcc ON tcc.dated = tbc.dated
WHERE tbc.dated BETWEEN ? AND ?
`, [started, ended])) - 1;
stmts.push(`
DROP TEMPORARY TABLE IF EXISTS
tmp.timeControlCalculate,
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];
};

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 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 => {
return subordinate.workerFk == id;
});
const isHr = await models.Account.hasRole(myUserId, 'hr');
const isHr = await models.Account.hasRole(myUserId, 'hr', myOptions);
if (isHr || isSubordinate)
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 userId = ctx.req.accessToken.userId;
const stmts = [];
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [userId]));
const queryIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1;
stmts.push('DROP TEMPORARY TABLE tmp.subordinate');
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const result = await conn.executeStmt(sql, myOptions);
return result[queryIndex];
};

View File

@ -5,18 +5,34 @@ describe('Worker createAbsence()', () => {
const workerId = 18;
it('should return an error for a user without enough privileges', async() => {
const ctx = {req: {accessToken: {userId: 18}}};
const absenceTypeId = 1;
const dated = new Date();
const ctx = {
req: {accessToken: {userId: 18}},
args: {
businessFk: 18,
absenceTypeId: 1,
dated: new Date()
}
};
let error;
await app.models.Worker.createAbsence(ctx, workerId, absenceTypeId, dated).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
const tx = await app.models.Calendar.beginTransaction({});
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() => {
@ -24,7 +40,14 @@ describe('Worker createAbsence()', () => {
accessToken: {userId: 19},
headers: {origin: 'http://localhost'}
};
const ctx = {req: activeCtx};
const ctx = {
req: activeCtx,
args: {
businessFk: 18,
absenceTypeId: 1,
dated: new Date()
}
};
ctx.req.__ = value => {
return value;
};
@ -32,17 +55,23 @@ describe('Worker createAbsence()', () => {
active: activeCtx
});
const absenceTypeId = 1;
const dated = new Date();
const createdAbsence = await app.models.Worker.createAbsence(ctx, workerId, absenceTypeId, dated);
const tx = await app.models.Calendar.beginTransaction({});
const expectedBusinessId = 18;
const expectedAbsenceTypeId = 1;
try {
const options = {transaction: tx};
expect(createdAbsence.businessFk).toEqual(expectedBusinessId);
expect(createdAbsence.dayOffTypeFk).toEqual(expectedAbsenceTypeId);
const createdAbsence = await app.models.Worker.createAbsence(ctx, workerId, options);
// Restores
await app.models.Calendar.destroyById(createdAbsence.id);
const expectedBusinessId = 18;
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 => {
return value;
};
let createdAbsence;
beforeEach(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
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() => {
activeCtx.accessToken.userId = 106;
const tx = await app.models.Calendar.beginTransaction({});
let error;
await app.models.Worker.deleteAbsence(ctx, 18, createdAbsence.id).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
try {
const options = {transaction: tx};
const createdAbsence = await app.models.Calendar.create({
businessFk: businessId,
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;
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"
},
"started": {
"type": "Date"
"type": "date"
},
"ended": {
"type": "Date"
"type": "date"
}
},
"relations": {
@ -38,6 +38,11 @@
"type": "belongsTo",
"model": "WorkCenterHoliday",
"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/activeWithRole')(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>
<vn-side-menu side="right">
<div class="vn-pa-md">
<div class="totalBox" style="text-align: center;">
<h6 translate>Holidays</h6>
<div class="totalBox vn-mb-sm" style="text-align: center;">
<h6>{{'Contract' | translate}} ID: {{$ctrl.businessId}}</h6>
<div>
{{'Used' | translate}} {{$ctrl.calendar.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.calendar.totalHolidays || 0}} {{'days' | translate}}
{{'Used' | translate}} {{$ctrl.contractHolidays.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.contractHolidays.totalHolidays || 0}} {{'days' | translate}}
</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">
<vn-autocomplete label="Year"
data="$ctrl.yearFilter"
@ -37,8 +46,23 @@
value-field="year"
order="DESC">
</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 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}"
ng-click="$ctrl.pick(absenceType)">
<vn-avatar

View File

@ -20,7 +20,24 @@ class Controller extends Section {
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() {
@ -31,16 +48,6 @@ class Controller extends Section {
this._date = value;
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);
for (let i = 0; i < this.months.length; i++) {
@ -59,26 +66,51 @@ class Controller extends Section {
this._worker = value;
if (value) {
this.refresh().then(() => this.repaint());
this.getIsSubordinate();
this.getActiveContract();
}
}
buildYearFilter() {
const currentYear = new Date().getFullYear();
const minRange = currentYear - 5;
const now = new Date();
now.setFullYear(now.getFullYear() + 1);
const maxYear = now.getFullYear();
const minRange = maxYear - 5;
const years = [];
for (let i = currentYear; i > minRange; i--)
for (let i = maxYear; i > minRange; i--)
years.push({year: i});
this.yearFilter = years;
}
getIsSubordinate() {
this.$http.get(`Workers/${this.worker.id}/isSubordinate`).then(res =>
this.isSubordinate = res.data
);
this.$http.get(`Workers/${this.worker.id}/isSubordinate`)
.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) {
@ -155,9 +187,6 @@ class Controller extends Section {
if (!this.absenceType)
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 stamp = day.getTime();
const event = this.events[stamp];
@ -176,7 +205,8 @@ class Controller extends Section {
const absenceType = this.absenceType;
const params = {
dated: dated,
absenceTypeId: absenceType.id
absenceTypeId: absenceType.id,
businessFk: this.businessId
};
const path = `Workers/${this.$params.id}/createAbsence`;
@ -190,7 +220,10 @@ class Controller extends Section {
};
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;
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()];
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() {
const params = {
workerFk: this.worker.id,
started: this.started,
ended: this.ended
businessFk: this.businessId,
year: this.year
};
return this.$http.get(`Calendars/absences`, {params})
.then(res => this.onData(res.data));

View File

@ -41,35 +41,31 @@ describe('Worker', () => {
});
});
describe('started property', () => {
it(`should return first day and month of current year`, () => {
let started = new Date(year, 0, 1);
describe('businessId() setter', () => {
it(`should set the contract id and then call to the refresh method`, () => {
jest.spyOn(controller, 'refresh').mockReturnValue(Promise.resolve());
expect(controller.started).toEqual(started);
});
});
controller.businessId = 106;
describe('ended property', () => {
it(`should return last day and month of current year`, () => {
let ended = new Date(year, 11, 31);
expect(controller.ended).toEqual(ended);
expect(controller.refresh).toHaveBeenCalledWith();
});
});
describe('months property', () => {
it(`should return an array of twelve months length`, () => {
const started = new Date(year, 0, 1);
const ended = new Date(year, 11, 1);
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);
});
});
describe('worker() setter', () => {
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 tomorrow = new Date(today.getTime());
@ -78,49 +74,60 @@ describe('Worker', () => {
let yesterday = new Date(today.getTime());
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};
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();
let events = controller.events;
expect(controller.isSubordinate).toBe(true);
});
});
expect(events[today.getTime()].name).toEqual('New year, Holiday');
expect(events[tomorrow.getTime()].name).toEqual('Easter');
expect(events[yesterday.getTime()].name).toEqual('Leave');
expect(events[yesterday.getTime()].color).toEqual('#bbb');
expect(controller.getIsSubordinate).toHaveBeenCalledWith();
describe('getActiveContract()', () => {
it(`should return the current contract and then set the businessId property`, () => {
jest.spyOn(controller, 'refresh').mockReturnValue(Promise.resolve());
$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()', () => {
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')
.respond({
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;
const dayElement = angular.element('<div><section></section></div>')[0];
const dayNumber = dayElement.firstElementChild;
controller.formatDay(today, dayElement);
@ -160,28 +167,6 @@ describe('Worker', () => {
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`, () => {
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`, () => {
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 expectedParams = {workerFk: 106, started: started, ended: ended};
const expectedParams = {year: year};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expect('GET', `Calendars/absences?${serializedParams}`).respond(200, expecteResponse);
controller.refresh();

View File

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

View File

@ -13,6 +13,19 @@ class Controller extends Section {
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
*/
@ -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() {
const params = {workerFk: this.$params.id};
const filter = {
@ -80,7 +98,6 @@ class Controller extends Section {
};
this.$.model.applyFilter(filter, params).then(() => {
this.getWorkedHours(this.started, this.ended);
this.getAbsences();
});
}
@ -89,10 +106,10 @@ class Controller extends Section {
}
getAbsences() {
const fullYear = this.started.getFullYear();
let params = {
workerFk: this.$params.id,
started: this.started,
ended: this.ended
businessFk: this.businessId,
year: fullYear
};
return this.$http.get(`Calendars/absences`, {params})
@ -257,5 +274,8 @@ Controller.$inject = ['$element', '$scope', 'vnWeekDays'];
ngModule.vnComponent('vnWorkerTimeControl', {
template: require('./index.html'),
controller: Controller
controller: Controller,
bindings: {
worker: '<'
}
});