Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Defaults to `table`. `csv` and `markdown` can be printed to stdout, `pdf` and `x
There are additional options for the invoice output as given in the following example:

```shell
gtt report --output=invoice --file=invoice.md --from 2021-02-01 --to 2021-02-28 --closed --invoiceCurrencyMaxUnit 1 --invoiceTitle "Rechnung" --invoiceAddress "Firma" "Mr. X" "Strasse" "10000 Ort" "Land" --invoiceCurrency "EUR" --invoiceCurrencyPerHour "50" --invoiceVAT "0.15" --invoiceDate "1.03.2021"
gtt report --output=invoice --file=invoice.md --from 2021-02-01 --to 2021-02-28 --closed --invoiceCurrencyMaxUnit 1 --invoiceTitle "Rechnung" --invoiceAddress "Firma" "Mr. X" "Strasse" "10000 Ort" "Land" --invoiceCurrency "EUR" --invoiceCurrencyPerHour "50" --invoiceVAT "0.15" --invoiceDate "1.03.2021" --invoicePositionText "Position Text"
```

For paper invoice, further process the output with a css, see the folder preview (styles.css, invoice.pdf)
Expand Down Expand Up @@ -658,7 +658,6 @@ invoiceSettings:
opening:
- Satz 1.
- Satz 2.
positionText: Positionstext
closing:
- Grussformel
-
Expand Down
2 changes: 1 addition & 1 deletion src/gtt-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ if (program.opts().local) {
config.assertLocalConfig();
}

Fs.open(program.opts().local ? config.local : config.global);
Fs.open(program.opts().local ? config.local : config.global);
39 changes: 39 additions & 0 deletions src/gtt-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const program = require('commander');
const colors = require('colors');
const moment = require('moment');
const Table = require('cli-table');


const Config = require('./include/file-config');
const Cli = require('./include/cli');
const Tasks = require('./include/tasks');

program
.arguments('[project]')
.option('--verbose', 'show verbose output')
.option('-c, --closed', 'show closed issues (instead of opened only)')
.option('--my', 'show only issues assigned to me')
.parse(process.argv);

Cli.verbose = program.verbose;

let config = new Config(process.cwd()),
tasks = new Tasks(config),
type = program.type ? program.type : 'issue',
project = program.args[0];

tasks.list(project, program.closed ? 'closed' : 'opened', program.my)
.then(issues => {
let table = new Table({
style : {compact : true, 'padding-left' : 1}
});
if (issues.length == 0) {
console.log("No issues found.");
}
issues.forEach(issue => {
table.push([issue.iid.toString().magenta, issue.title.green + "\n" + issue.data.web_url.gray, issue.state])
})
console.log(table.toString());
})
.catch(error => Cli.error(error));

2 changes: 1 addition & 1 deletion src/gtt-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ tasks.log()
});
}
)
.catch(error => Cli.error(error));
.catch(error => Cli.error(error));
2 changes: 2 additions & 0 deletions src/gtt-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ program
.option('--invoiceVAT <number>', 'vat decimal (20% = 0.2)')
.option('--invoiceDate <number>', 'date string')
.option('--invoiceCurrencyMaxUnit <number>', 'rouning invoice total, e.g. 0.01, 0.05 or 1')
.option('--invoicePositionText <text>', 'invoice position text')
.parse(process.argv);

// init helpers
Expand Down Expand Up @@ -138,6 +139,7 @@ config
.set('invoiceVAT', program.opts().invoiceVAT)
.set('invoiceDate', program.opts().invoiceDate)
.set('invoiceCurrencyMaxUnit', program.opts().invoiceCurrencyMaxUnit)
.set('invoicePositionText', program.opts().invoicePositionText)
.set('_createDump', program.opts().output === 'dump');

// date shortcuts
Expand Down
1 change: 1 addition & 0 deletions src/gtt.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ program
.command('stop', 'stop monitoring time')
.command('resume [project]', 'resume monitoring time for last stopped record')
.command('cancel', 'cancel and discard active monitoring time')
.command('list [project]', 'list all open issues')
.command('log', 'log recorded time records')
.command('sync', 'sync local time records to GitLab')
.command('edit [id]', 'edit time record by the given id')
Expand Down
14 changes: 10 additions & 4 deletions src/models/hasTimes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class hasTimes extends Base {
constructor(config) {
super(config);
this.times = [];
this.timesWarnings = [];
}

/**
Expand Down Expand Up @@ -58,6 +59,7 @@ class hasTimes extends Base {
*/
getTimes() {
let times = [],
timesWarnings = [],
timeSpent = 0,
totalTimeSpent = 0,
timeUsers = {},
Expand Down Expand Up @@ -127,18 +129,22 @@ class hasTimes extends Base {
!(created.isSameOrAfter(moment(this.config.get('from'))) && created.isSameOrBefore(moment(this.config.get('to'))))
) return resolve();

// warn about difference, but do not correct as gitlab API
// stats forget the times after an issue is moved to another project.
let difference = this.data.time_stats.total_time_spent - totalTimeSpent,
note = Object.assign({noteable_type: this._typeSingular}, this.data);

times.unshift(new Time(Time.toHumanReadable(difference, this.config.get('hoursPerDay')), null, note, this, this.config));

note.timeWarning = {};
note.timeWarning['stats'] = this.data.time_stats.total_time_spent;
note.timeWarning['notes'] = totalTimeSpent;
timesWarnings.push(new Time(Time.toHumanReadable(difference, this.config.get('hoursPerDay')), null, note, this, this.config));
resolve();
}));

promise.then(() => {
_.each(timeUsers, (time, name) => this[`time_${name}`] = Time.toHumanReadable(time, this.config.get('hoursPerDay'), timeFormat));
this.timeSpent = timeSpent;
this.times = times
this.times = times;
this.timesWarnings = timesWarnings;
});

return promise;
Expand Down
17 changes: 17 additions & 0 deletions src/models/issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ class issue extends hasTimes {
return promise;
}

list(project, state, my) {
return new Promise((resolve, reject) => {
let promise;
const query = `scope=${my ? "assigned-to-me" : "all"}&state=${state}`;
if (project) {
promise = this.get(`projects/${encodeURIComponent(project)}/issues?${query}`);
} else {
promise = this.get(`issues/?${query}`);
}
promise.then(response => {
const issues = response.body.map(issue => new this.constructor(this.config, issue))
resolve(issues)
});
promise.catch(error => reject(error))
})
}

/*
* properties
*/
Expand Down
12 changes: 9 additions & 3 deletions src/models/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ class report extends Base {
*/
getMergeRequests() {
let promise = this.all(`projects/${this.project.id}/merge_requests${this.params()}`);
promise.then(mergeRequests => this.mergeRequests = mergeRequests);
let excludes = this.config.get('excludeByLabels');
promise.then(mergeRequests => this.mergeRequests = mergeRequests.filter(mr => (
excludes.filter(l=>mr.labels.includes(l)).length==0 // keep all merge requests not including a exclude label
)));

return promise;
}
Expand All @@ -90,8 +93,11 @@ class report extends Base {
*/
getIssues() {
let promise = this.all(`projects/${this.project.id}/issues${this.params()}`);
promise.then(issues => this.issues = issues);

let excludes = this.config.get('excludeByLabels');
promise.then(issues => this.issues = issues.filter(issue => (
issue.moved_to_id == null && // filter moved issues in any case
excludes.filter(l=>issue.labels.includes(l)).length==0 // keep all issues not including a exclude label
)));
return promise;
}

Expand Down
28 changes: 28 additions & 0 deletions src/output/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ class base {
this.write(this.formats.headline(string));
}

/**
* print a headline for warnings
* @param string
*/
warningHeadline(string) {
if (this.config.get('noWarnings')) return;
this.headline(string);
}
/**
* print a warning
* @param string
Expand Down Expand Up @@ -104,6 +112,9 @@ class base {
let users = {};
let projects = {};
let times = [];
let timesWarnings = [];
let days = {};
let daysMoment = {};

let spentFreeLabels = this.config.get('freeLabels');
if(undefined === spentFreeLabels) {
Expand All @@ -113,11 +124,24 @@ class base {
['issues', 'mergeRequests'].forEach(type => {
this.report[type].forEach(issue => {
issue.times.forEach(time => {
let dateGrp = time.date.format(this.config.get('dateFormatGroupReport'));
if (!users[time.user]) users[time.user] = 0;
if (!projects[time.project_namespace]) projects[time.project_namespace] = 0;
if (!days[dateGrp]) {
days[dateGrp] = {}
daysMoment[dateGrp] = time.date;
};
if(!days[dateGrp][time.project_namespace]) {
days[dateGrp][time.project_namespace] = {};
}
if(!days[dateGrp][time.project_namespace][time.iid]) {
days[dateGrp][time.project_namespace][time.iid] = 0;
}


users[time.user] += time.seconds;
projects[time.project_namespace] += time.seconds;
days[dateGrp][time.project_namespace][time.iid] += time.seconds;

spent += time.seconds;
//if(time.parent.labels)
Expand All @@ -132,6 +156,7 @@ class base {
}
times.push(time);
});
issue.timesWarnings.forEach(warning => timesWarnings.push(warning));

totalEstimate += parseInt(issue.stats.time_estimate);
totalSpent += parseInt(issue.stats.total_time_spent);
Expand All @@ -152,6 +177,8 @@ class base {
return a.date.isBefore(b.date) ? 1 : -1;
});

this.days = days;
this.daysMoment = daysMoment;
this.users = _.mapObject(users, user => this.config.toHumanReadable(user, 'stats'));
this.projects = _.mapObject(projects, project => this.config.toHumanReadable(project, 'stats'));
this.stats = {
Expand All @@ -164,6 +191,7 @@ class base {
this.spent = spent;
this.spentFree = spentFree;
this.totalSpent = totalSpent;
this.timesWarnings = timesWarnings;
}

/**
Expand Down
58 changes: 47 additions & 11 deletions src/output/invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ class invoice extends Base {
this.invoiceCurrency = this.config.get('invoiceCurrency');
this.invoiceCurrencyPerHour = this.config.get('invoiceCurrencyPerHour');
this.invoiceVAT = this.config.get('invoiceVAT');
this.invoicePositionText = this.config.get('invoicePositionText');
this.invoiceCurrencyMaxUnit = this.config.get('invoiceCurrencyMaxUnit');
this.totalhForInvoice = (this.spent-this.spentFree) / 3600.0;
this.totalForInvoiceExkl = this.totalhForInvoice * this.invoiceCurrencyPerHour;
this.totalForInvoiceMwst = this.totalForInvoiceExkl * this.invoiceVAT;
this.totalForInvoice = this.totalForInvoiceExkl + this.totalForInvoiceMwst;
// round
this.totalForInvoice = Math.round(this.totalForInvoice/this.invoiceCurrencyMaxUnit)*this.invoiceCurrencyMaxUnit;
// round subtotals to 0.01 and total to invoiceCurrencyMaxUnit.
this.totalForInvoiceExkl = Math.round(this.totalhForInvoice * this.invoiceCurrencyPerHour * 100) * 0.01;
this.totalForInvoiceMwst = Math.round(this.totalhForInvoice * this.invoiceCurrencyPerHour * this.invoiceVAT * 100) * 0.01;
this.totalForInvoice = Math.round((this.totalForInvoiceExkl + this.totalForInvoiceMwst)/this.invoiceCurrencyMaxUnit)*this.invoiceCurrencyMaxUnit;
}


Expand Down Expand Up @@ -57,7 +57,7 @@ class invoice extends Base {
let from = this.concat(this.config.get('invoiceSettings').from, '</br>');
let opening = this.concat(this.config.get('invoiceSettings').opening, '</br>');
let closing = this.concat(this.config.get('invoiceSettings').closing, '</br>');
let positionText = this.concat(this.config.get('invoiceSettings').positionText, '</br>');


this.out +=
`<div class="senderBox">${from}</div>
Expand All @@ -73,7 +73,7 @@ class invoice extends Base {
${opening}

<div class="positionBox">
<div class="positionDesc">${positionText} (${this.totalhForInvoice.toFixed(2)} Stunden zu ${this.invoiceCurrencyPerHour} ${this.invoiceCurrency})</div>
<div class="positionDesc">${this.invoicePositionText} (${this.totalhForInvoice.toFixed(2)} Stunden zu ${this.invoiceCurrencyPerHour} ${this.invoiceCurrency})</div>
<div class="positionValue">${this.invoiceCurrency} ${this.totalForInvoiceExkl.toFixed(2)}</div>
<div class="positionDesc">MWST (${this.invoiceVAT*100}%)</div>
<div class="positionValue">${this.invoiceCurrency} ${this.totalForInvoiceMwst.toFixed(2)}</div>
Expand All @@ -89,7 +89,23 @@ ${closing}
<h1 style="page-break-before: always;"><br/><br/>Stundenrapport</h1>`;

this.headline('Total');
this.write(stats.substr(1));
//this.write(stats.substr(1));
this.write(this.config.toHumanReadable(this.spent, 'stats'));
this.write(this.config.toHumanReadable(this.spentFree, 'statsFree'));

// warnings
let warnings = '';

this.timesWarnings.forEach( warning => {
let stats = this.config.toHumanReadable(warning.data.timeWarning.stats, 'stats');
let notes = this.config.toHumanReadable(warning.data.timeWarning.notes, 'stats');
warnings += `\n* ${warning.data.iid} ${warning.data.title}: Difference between stats and notes of ${warning.time}.`;
warnings += `<br/>Stats: ${stats}, Notes: ${notes}`
});
if(warnings != '') {
this.warningHeadline('Warnings');
this.warning(warnings+'\n');
}
}

makeIssues() {
Expand Down Expand Up @@ -117,11 +133,31 @@ ${closing}
}

makeRecords() {
this.headline('Details');
this.out += `

let times = [this.config.get('recordColumns').map(c => c.replace('_', ' '))];
this.times.forEach(time => times.push(this.prepare(time, this.config.get('recordColumns'))));
<h1 style="page-break-before: always;"><br/><br/>Stundenrapport detailliert</h1>`;
this.headline('Details');

let times = [['date', 'project', 'iid', 'time']];
let days = Object.keys(this.days);
days.sort();
days.forEach(
k => {
let day = this.days[k];
let refD = this.daysMoment[k].format(this.config.get('dateFormat'));
let projects = Object.keys(day);
projects.forEach(
p => {
let iids = Object.keys(day[p]);
iids.sort();
iids.forEach(
iid => {
times.push([refD, p, iid, this.config.toHumanReadable(day[p][iid], 'records')]);
});
});
});
//let times = [this.config.get('recordColumns').map(c => c.replace('_', ' '))];
//this.times.forEach(time => times.push(this.prepare(time, this.config.get('recordColumns'))));
this.write(Table(times));
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/output/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,37 @@ class table extends Base {
this.write(mergeRequests.toString());
}

makeDailyStats() {
this.headline('DAILY RECORDS');

var tabledt = new Table({head: ['date', 'time']});
var tabledit = new Table({head: ['date', 'project', 'iid', 'time']});
let days = Object.keys(this.days);
days.sort();
days.forEach(
k => {
let day = this.days[k];
let refD = this.daysMoment[k].format(this.config.get('dateFormat'));
let projects = Object.keys(day);
let time = 0;
projects.forEach(
p => {
let iids = Object.keys(day[p]);
iids.sort();
iids.forEach(
iid => {
tabledit.push([refD, p, iid, this.config.toHumanReadable(day[p][iid], 'records')]);
time += day[p][iid];
});
});
tabledt.push([refD, this.config.toHumanReadable(time)]);
});
this.write(tabledt.toString());
this.write(tabledit.toString());
}

makeRecords() {
this.makeDailyStats();
this.headline('TIME RECORDS');
let times = new Table({head: this.config.get('recordColumns').map(c => c.replace('_', ' '))});
this.times.forEach(time => times.push(this.prepare(time, this.config.get('recordColumns'))));
Expand Down