diff --git a/src/libs/models/reportgenerator/ReportGeneratorOdt.cpp b/src/libs/models/reportgenerator/ReportGeneratorOdt.cpp index 13e72d20..86068f2a 100644 --- a/src/libs/models/reportgenerator/ReportGeneratorOdt.cpp +++ b/src/libs/models/reportgenerator/ReportGeneratorOdt.cpp @@ -1,1339 +1,1360 @@ /* This file is part of the KDE project Copyright (C) 2017 Dag Andersen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // clazy:excludeall=qstring-arg #include "planmodels_export.h" #include "ReportGeneratorOdt.h" #include "kptproject.h" #include "kptschedule.h" #include "kptnodeitemmodel.h" #include "kpttaskstatusmodel.h" #include "kptitemmodelbase.h" #include "kptnodechartmodel.h" #include "kptschedulemodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define HeaderRole Qt::UserRole + 543 const QLoggingCategory &PLANRG_LOG() { static const QLoggingCategory category("calligra.plan.reportodt"); return category; } #define dbgRG qCDebug(PLANRG_LOG)<(p->sourceModel())) { ChartItemModel *c = qobject_cast(p->sourceModel()); if (c) { return c; } } return 0; } bool startsWith(const QStringList &keys, const QString &key) { for (const QString &k : keys) { - if (key.startsWith(k)) { + if (key.toLower().startsWith(k)) { return true; } } return false; } QStringList trimmed(const QStringList &lst) { QStringList rlst; for (const QString &s : lst) { QString r = s.trimmed(); if (!r.isEmpty()) { rlst << r; } } return rlst; } bool addDataToFile(QByteArray &buffer, const QString &destName, KoStore &to) { QBuffer file(&buffer); if (!file.open(QIODevice::ReadOnly)) { dbgRG<<"Failed to open buffer"; return false; } if (!to.open(destName)) { dbgRG<<"Failed to open file for writing:"< 0; total += block) { data.resize(block); if (to.write(data) != block) { dbgRG<<"Failed to write block of data"; return false; } data.resize(8*1024); } to.close(); file.close(); return true; } QAbstractItemModel *translationModel() { QList > names; names << QPair("Project", i18n("Project")) << QPair("Manager", i18n("Manager")) << QPair("Schedule", i18n("Schedule")) << QPair("BCWS", xi18nc("@title:column Budgeted Cost of Work Scheduled", "BCWS")) << QPair("BCWP", xi18nc("@title:column Budgeted Cost of Work Performed", "BCWP")) << QPair("ACWP", xi18nc("@title:column Actual Cost of Work Performed", "ACWP")) << QPair("SPI", xi18nc("@title:column Schedule Performance Index", "SPI")) << QPair("CPI", xi18nc("@title:column Cost Performance Index", "CPI")); - QStandardItemModel *model = new QStandardItemModel(); + QStandardItemModel *model = new QStandardItemModel(0, names.count()); for (int column = 0; column < names.count(); ++column) { model->setHeaderData(column, Qt::Horizontal, names.at(column).first, HeaderRole); model->setHeaderData(column, Qt::Horizontal, names.at(column).second); } return model; } QAbstractItemModel *projectModel() { QList > names; names << QPair("Name", i18n("Name")) << QPair("Manager", i18n("Manager")) << QPair("BCWS Cost", i18nc("Cost based Budgeted Cost of Work Scheduled", "BCWS Cost")) << QPair("BCWP Cost", i18nc("Cost based Budgeted Cost of Work Performed", "BCWP Cost")) << QPair("ACWP Cost", i18nc("Cost based Actual Cost of Work Performed", "ACWP Cost")) << QPair("SPI Cost", i18nc("Cost based Schedule Performance Index", "SPI Cost")) << QPair("CPI Cost", i18nc("Cost based Cost Performance Index", "CPI Cost")) << QPair("BCWS Effort", i18nc("Effort based Budgeted Cost of Work Scheduled", "BCWS Effort")) << QPair("BCWP Effort", i18nc("Effort based Budgeted Cost of Work Performed", "BCWP Effort")) << QPair("ACWP Effort", i18nc("Effort based Actual Cost of Work Performed", "ACWP Effort")) << QPair("SPI Effort", i18nc("Effort based Schedule Performance Index", "SPI Effort")) << QPair("CPI Effort", i18nc("Effort based Cost Performance Index", "CPI Effort")); QStandardItemModel *model = new QStandardItemModel(0, names.count()); for (int column = 0; column < names.count(); ++column) { model->setHeaderData(column, Qt::Horizontal, names.at(column).first, HeaderRole); model->setHeaderData(column, Qt::Horizontal, names.at(column).second); } return model; } void initProjectModel(QAbstractItemModel *model, Project *project, ScheduleManager *sm) { if (model->rowCount() == 0) { model->insertRow(0); } QModelIndex idx = model->index(0, 0); model->setData(idx, project->name()); idx = model->index(0, 1); model->setData(idx, project->leader()); PerformanceDataCurrentDateModel m(0); m.setProject(project); m.setScheduleManager(sm); m.setNodes(QList() << project); int col = 2; // column of BCWS Cost for (int r = 0; r < 2; ++r) { for (int c = 0; c < 5; ++c) { idx = model->index(0, col++); QModelIndex cidx = m.index(r, c); model->setData(idx, cidx.data()); } } } QAbstractItemModel *scheduleModel() { ScheduleItemModel m; QStandardItemModel *model = new QStandardItemModel(0, m.columnCount()); for (int c = 0; c < m.columnCount(); ++c) { model->setHeaderData(c, Qt::Horizontal, m.headerData(c, Qt::Horizontal)); model->setHeaderData(c, Qt::Horizontal, m.headerData(c, Qt::Horizontal, Qt::EditRole), HeaderRole); } return model; } void initScheduleModel(QAbstractItemModel *model, Project *project, ScheduleManager *sm) { ScheduleItemModel m; m.setProject(project); QModelIndex idx = m.index(sm); if (idx.isValid()) { if (model->rowCount() == 0) { model->insertRow(0); } for (QModelIndex i = idx; i.isValid(); i = i.sibling(i.row(), i.column() + 1)) { QModelIndex midx = model->index(0, i.column()); model->setData(midx, i.data()); - dbgRGVariable<headerData(midx.column(), Qt::Horizontal, HeaderRole).toString()<<'='<headerData(midx.column(), Qt::Horizontal, HeaderRole).toString()<<'='<rowCount()<columnCount(); } //-------------------------------------- ReportGeneratorOdt::ReportGeneratorOdt() : ReportGenerator() , m_templateStore(0) { m_keys = QStringList() << "table" << "chart"; m_variables = QStringList() << "project" << "schedule"; m_basemodels << new NodeItemModel() << new TaskStatusItemModel() << new ChartItemModel() << new ScheduleItemModel(); - m_datamodels["tasks"] = m_basemodels.at(0); - m_headerrole["tasks"] = Qt::EditRole; - m_datamodels["taskstatus"] = m_basemodels.at(1); - m_headerrole["taskstatus"] = Qt::EditRole; - m_datamodels["chart.project"] = m_basemodels.at(2); - m_headerrole["chart.project"] = Qt::EditRole; - m_datamodels["projects"] = projectModel(); - m_headerrole["projects"] = HeaderRole; - m_datamodels["schedules"] = m_basemodels.at(3); - m_headerrole["schedules"] = Qt::EditRole; - - m_datamodels["project"] = projectModel(); - m_headerrole["project"] = HeaderRole; - - m_datamodels["schedule"] = scheduleModel(); - m_headerrole["schedule"] = HeaderRole; - - m_datamodels["tr"] = translationModel(); - m_headerrole["tr"] = HeaderRole; + addDataModel("tasks", m_basemodels.at(0), Qt::EditRole); + addDataModel("taskstatus", m_basemodels.at(1), Qt::EditRole); + addDataModel("chart.project", m_basemodels.at(2), Qt::EditRole); + addDataModel("projects", projectModel(), HeaderRole); + addDataModel("schedules", m_basemodels.at(3), Qt::EditRole); + addDataModel("project", projectModel(), HeaderRole); + addDataModel("schedule", scheduleModel(), HeaderRole); + addDataModel("tr", translationModel(), HeaderRole); + +// for (QAbstractItemModel *m : m_datamodels) { +// const QString key = m_datamodels.key(m); +// qInfo()<columnCount(); +// for (int i = 0; i < m->columnCount(); ++i) { +// qInfo()<<'\t'<headerData(i, Qt::Horizontal, m_headerrole.value(key)).toString(); +// } +// } } ReportGeneratorOdt::~ReportGeneratorOdt() { for (QAbstractItemModel *m : m_datamodels) { // clazy:exclude=range-loop if (!m_basemodels.contains(qobject_cast(m))) { delete m; } } qDeleteAll(m_basemodels); close(); } +void ReportGeneratorOdt::addDataModel(const QString &name, QAbstractItemModel *model, int role) +{ + model->setObjectName(name); + m_datamodels[name] = model; + m_headerrole[name] = role; +} + bool ReportGeneratorOdt::open() { m_lastError.clear(); if (m_templateFile.isEmpty()) { m_lastError = i18n("Missing report template file"); return false; } if (m_reportFile.isEmpty()) { m_lastError = i18n("Missing report result file"); return false; } if (m_templateStore) { m_lastError = i18n("Report generator is already open"); return false; } + if (!QFile::exists(m_templateFile)) { + m_lastError = i18n("Report template file does not exist"); + return false; + } m_templateStore = KoStore::createStore(m_templateFile, KoStore::Read); if (!m_templateStore) { dbgRG<<"Failed to open store:"<setProject(m_project); m->setScheduleManager(m_manager); if (qobject_cast(m)) { qobject_cast(m)->setNodes(QList() << m_project); dbgRGChart<<"chart:"<(m_datamodels["schedules"])->setProject(m_project); return true; } void ReportGeneratorOdt::close() { delete m_templateStore; m_templateStore = 0; } bool ReportGeneratorOdt::createReport() { if (!m_templateStore) { m_lastError = i18n("Report generator has not been correctly opened"); return false; } // TODO get mimetype return createReportOdt(); } bool ReportGeneratorOdt::createReportOdt() { m_tags.clear(); dbgRG<<"url:"<urlOfStore(); KoOdfReadStore reader(m_templateStore); if (!reader.loadAndParse(m_lastError)) { dbgRG<<"Failed to loadAndParse:"<endElement(); // office:document-content writer->endDocument(); if (!outStore->addDataToFile(buffer.buffer(), "content.xml")) { dbgRG<<"Failed to open 'content.xml' for writing"; m_lastError = i18n("Failed to write to store: %1", QString("content.xml")); delete writer; delete outStore; return false; } buffer.close(); if (m_manifestfiles.contains("styles.xml")) { dbgRG << endl << "---- treat styles.xml (for master-page headers/footers) ----" << endl; QBuffer buffer2; KoXmlWriter *styles = createOasisXmlWriter(reader, &buffer2, "styles.xml", "office:document-styles"); if (!styles) { dbgRG<<"Failed to create styles.xml writer"; return false; } KoXmlDocument stylesDoc; if (!reader.loadAndParse("styles.xml", stylesDoc, m_lastError)) { debugPlan<<"Failed to read styles.xml"<endElement(); // office:document-styles styles->endDocument(); if (!outStore->addDataToFile(buffer2.buffer(), "styles.xml")) { dbgRG<<"Failed to open 'styles.xml' for writing"; m_lastError = i18n("Failed to write to store: %1", QString("styles.xml")); delete writer; delete outStore; } m_manifestfiles.removeAt(m_manifestfiles.indexOf("styles.xml")); } dbgRG << endl << "---- treat the embedded files ----" << endl; treatEmbededObjects(reader, *outStore); dbgRG << endl << "---- copy rest of files ----" << endl; for (int i = 0; i < m_manifestfiles.count(); ++i) { copyFile(*reader.store(), *outStore, m_manifestfiles.at(i)); } if (!outStore->finalize()) { dbgRG<<"Failed to write store:"<urlOfStore(); m_lastError = i18n("Failed to write report file: %1", outStore->urlOfStore().path()); delete writer; delete outStore; return false; } delete writer; delete outStore; dbgRG<<"finished"; return true; } bool ReportGeneratorOdt::handleTextP(KoXmlWriter &writer, const KoXmlElement &textp) { Q_UNUSED(writer); dbgRG<<"Check:"<finish(); dbgRG<<"Found:"<type + '.' + field->dataName; field->setModel(dataModel(modelName), m_headerrole[modelName]); dbgRGChart<<"Found chart:"<seqNr = -1; + field->seqNr = USERFIELD_NONE; dbgRGTable<type != "table") { return false; } dbgRGTable; UserField *field = m_userfields[m_activefields.last()]; - field->seqNr = 0; // we are in header row + field->seqNr = USERFIELD_HEADER; // we are in header row return false; } bool ReportGeneratorOdt::treatTableRow(KoXmlWriter &writer, const KoXmlElement &rowElement) { if (m_activefields.isEmpty() || m_userfields[m_activefields.last()]->type != "table") { return false; } UserField *field = m_userfields[m_activefields.last()]; dbgRGTable<seqNr; - if (field->seqNr == -1) { + if (field->seqNr == USERFIELD_NONE) { // there is no header row, so start with data rows directly - field->seqNr = 1; + field->seqNr = USERFIELD_DATA; } - if (field->seqNr == 0) { + if (field->seqNr == USERFIELD_HEADER) { // header row writer.startElement("table:table-row"); writeElementAttributes(writer, rowElement); writeChildElements(writer, rowElement); writer.endElement(); - field->seqNr = 1; // next is first row + field->seqNr = USERFIELD_DATA; // next is first row } else { dbgRGTable<<" add rows:"<rowCount(); for (field->begin(); field->next();) { writer.startElement("table:table-row"); writeElementAttributes(writer, rowElement); writeChildElements(writer, rowElement); writer.endElement(); } + field->finish(); } return true; } ReportGeneratorOdt::UserField *ReportGeneratorOdt::findUserField(const KoXmlElement &decl) const { UserField *field = 0; QString name = decl.attributeNS(KoXmlNS::text, "name"); // eg: table1 or table1.Type or project.name or tr.bcws field = m_userfields.value(name); // if Variable or Translation if (!field) { QStringList lst = name.split('.'); QMap::const_iterator it; for (it = m_userfields.constBegin(); it != m_userfields.constEnd(); ++it) { if (lst.first().startsWith(it.key())) { field = it.value(); break; } } } return field; } void ReportGeneratorOdt::treatUserFieldGet(KoXmlWriter &writer, const KoXmlElement &e) { dbgRG<variant() == UserField::Rows) { QString name = e.attributeNS(KoXmlNS::text, "name"); // eg: table1.type QString value = e.text(); // eg: type or name QString data = field->data(value); writer.addTextNode(data); dbgRGTable<<"rows:"<variant() == UserField::Header) { QString name = e.attributeNS(KoXmlNS::text, "name"); // eg: table1.type QString value = e.text(); // eg: type or BCWS Cost QString data = field->headerData(value); writer.addTextNode(data); dbgRGTable<<"header row:"<variant() == UserField::Variable) { QString name = e.attributeNS(KoXmlNS::text, "name"); // eg: project.name dbgRGVariable<<"variable:"<columns.value(0)<data(field->columns.value(0)); writer.addTextNode(field->data(field->columns.value(0))); // a variable has only one column } else if (field->variant() == UserField::Translation) { QString name = e.attributeNS(KoXmlNS::text, "name"); // eg: tr.bcws QString data = i18n(field->dataName.toLatin1()); if (field->headerNames.contains(field->dataName)) { data = field->data(field->dataName); } dbgRGTr<<"translation:"<dataName<<'='<name = name; m_userfields[field->name] = field; field->dataName = value; - field->seqNr = -3; + field->seqNr = USERFIELD_TRANSLATION; field->columns << value; field->setModel(dataModel("tr"), m_headerrole["tr"]); dbgRGTr<<" added translation"<name<dataName; } else if (m_variables.contains(tags.first())) { Q_ASSERT(tags.count() >= 2); Q_ASSERT(!m_userfields.contains(tags.first())); UserField *field = new UserField(); field->name = name; m_userfields[field->name] = field; field->dataName = tags.first(); - field->seqNr = -2; + field->seqNr = USERFIELD_VARIABLE; QStringList vl = value.split(';'); if (!vl.isEmpty()) { field->columns << vl.takeFirst(); field->properties = vl; } field->setModel(dataModel(field->dataName), m_headerrole[field->dataName]); dbgRGVariable<<" added variable"<name<columns<properties; } else { for (const QString &k : m_keys) { // clazy:exclude=range-loop const QString vname = tags.first(); if (!vname.startsWith(k)) { continue; } if (tags.count() == 1) { - // this is the main definition (eg: name: "table1", value: "tasks ...") + // this is the main definition (eg: name: "table1", value: "project; values=bcws effort, bcwp effort, acwp effort;") if (!m_userfields.contains(vname)) { m_userfields[vname] = new UserField(); } UserField *field = m_userfields[vname]; field->name = vname; field->type = k; QStringList vl = trimmed(value.toLower().split(';', QString::SkipEmptyParts)); field->dataName = vl.takeFirst(); field->properties += vl; field->setModel(dataModel(field->dataName), m_headerrole[field->dataName]); if (k == "chart") { dbgRGChart<<" "<<"added tag:"<properties; } else { dbgRG<<" "<<"added tag:"<properties; } } else { // this is the fields column definitions (eg: name: "table1.type" value: "") if (!m_userfields.contains(vname)) { m_userfields[vname] = new UserField(); + UserField *field = m_userfields[vname]; + field->name = vname; } UserField *field = m_userfields[vname]; - field->name = vname; field->columns << value.trimmed().toLower(); + Q_ASSERT(field->name == vname); dbgRG<<" "<<"added column:"<name<columns; } } } } writer.startElement("text:user-field-decls"); writeElementAttributes(writer, decls); writeChartElements(writer, decls); writer.endElement(); } void ReportGeneratorOdt::writeElementAttributes(KoXmlWriter &writer, const KoXmlElement &element, const QStringList &exclude) { for (const QPair &a : element.attributeFullNames()) { // clazy:exclude=range-loop QString prefix = KoXmlNS::nsURI2NS(a.first); if (prefix.isEmpty()) { dbgRG<<" Skipping unknown namespace:"<"<hasFile("META-INF/manifest.xml")) { dbgRG<<"No manifest file"; return 0; } KoXmlDocument manifest; if (!reader.loadAndParse("META-INF/manifest.xml", manifest, m_lastError)) { dbgRG<<"Failed to read manifest:"<hasFile("mimetype")) { if (!copyFile(*reader.store(), *out, "mimetype")) { m_lastError = i18n("Failed to load manifest file"); delete out; return 0; } } if (!copyFile(*reader.store(), *out, "META-INF/manifest.xml")) { m_lastError = i18n("Failed to write manifest file"); delete out; return 0; } KoXmlElement e; forEachElement(e, manifest.documentElement()) { dbgRG<hasFile(fileName); if (reader.store()->isOpen()) { reader.store()->close(); } if (!reader.store()->open(fileName)) { dbgRG << "Entry " << fileName << " not found!"; m_lastError = xi18nc("@info", "Failed to open file %1 from store.", fileName); return 0; } if (!reader.store()->device()->isOpen()) { reader.store()->device()->open(QIODevice::ReadOnly); } KoXmlWriter *writer = 0; QXmlStreamReader xml(reader.store()->device()); xml.setNamespaceProcessing(true); while (!xml.atEnd()) { xml.readNext(); if (xml.tokenType() == QXmlStreamReader::StartElement && !xml.namespaceDeclarations().isEmpty()) { writer = new KoXmlWriter(buffer); writer->startDocument(rootElementName); writer->startElement(rootElementName); writer->addAttribute("xmlns:calligra", KoXmlNS::calligra); // Note: windows needs this type of iteration QXmlStreamNamespaceDeclarations dcl = xml.namespaceDeclarations(); for (int ns = 0; ns < dcl.count(); ++ns) { dbgRGTmp<<"add namespace:"<<("xmlns:" + dcl[ns].prefix())<addAttribute(("xmlns:" + dcl[ns].prefix()).toLatin1(), dcl[ns].namespaceUri().toUtf8()); } QXmlStreamAttributes attr = xml.attributes(); for (int a = 0; a < attr.count(); ++a) { dbgRGTmp<<"add attribute:"<addAttribute(attr[a].qualifiedName().toLatin1(), attr[a].value().toUtf8()); } } } reader.store()->close(); if (!writer) { dbgRG<<"Failed to find a start element with namespace declarations in"<%1", fileName); } return writer; } void ReportGeneratorOdt::treatEmbededObjects(KoOdfReadStore &reader, KoStore &outStore) { dbgRGChart; {QMap::const_iterator it; for (it = m_embededcharts.constBegin(); it != m_embededcharts.constEnd(); ++it) { treatChart(reader, outStore, it.key(), it.value()); }} {QMap::const_iterator it; for (it = m_embededgantts.constBegin(); it != m_embededgantts.constEnd(); ++it) { treatGantt(reader, outStore, it.key(), it.value()); }} } void ReportGeneratorOdt::treatChart(KoOdfReadStore &reader, KoStore &outStore, const QString &name, const QString &dir) { dbgRGChart<dataName == "project") { ChartItemModel *m = findChartItemModel(field->model); if (m) { m->setNodes(QList() << m_project); } if (field->properties.isEmpty()) { // default: take all for (const QString &c : field->headerNames) { // clazy:exclude=range-loop field->columns << c; } } else { QStringList values; for (const QString &p : field->properties) { // clazy:exclude=range-loop if (p.startsWith("values")) { QStringList vl = p.split("="); Q_ASSERT(vl.count() > 1); Q_ASSERT(vl.at(0) == "values"); for (const QString &v : vl.at(1).split(',')) { // clazy:exclude=range-loop values << v.toLower().trimmed(); } } } field->columns = values; } dbgRGChart<endElement(); // office:document-content writer->endDocument(); dbgRGChart<toString(); if (!outStore.addDataToFile(buffer.buffer(), file)) { dbgRGChart<<"Failed to open"<hasLabels = e.attributeNS(KoXmlNS::chart, "data-source-has-labels"); writer.startElement(tag.constData()); writeElementAttributes(writer, e); writeChartElements(writer, e); writer.endElement(); } else if (tag == "chart:categories") { // assume this is x-axis UserField *field = m_userfields[m_activefields.last()]; int columns = field->columnCount(); if (columns > 0) { writer.startElement(tag.constData()); writeElementAttributes(writer, e, QStringList() << "table:cell-range-address"); int startRow = 1; if (field->hasLabels == "both" || field->hasLabels == "primary-x") { ++startRow; } QString end = QString("local-table.$A$%1:$A$%2").arg(startRow).arg(startRow+field->rowCount()-1); writer.addAttribute("table:cell-range-address", end); writeChartElements(writer, e); writer.endElement(); } } else if (tag == "chart:series") { UserField *field = m_userfields[m_activefields.last()]; int columns = field->columnCount(); if (columns > 0 && field->serieNr < columns) { int startRow = 1; if (field->hasLabels == "both" || field->hasLabels == "primary-x") { ++startRow; } char startColumn = 'A'; if (field->hasLabels == "both" || field->hasLabels == "primary-y") { ++startColumn; } writer.startElement(tag.constData()); writeElementAttributes(writer, e, QStringList() << "chart:values-cell-range-address"<<"chart:label-cell-address"); QChar c(startColumn + field->serieNr); QString lab = QString("local-table.$%1$1").arg(c); writer.addAttribute("chart:values-cell-address", lab); QString end = QString("local-table.$%1$%2:$%1$%3").arg(c).arg(startRow).arg(startRow+field->rowCount()-1); writer.addAttribute("chart:values-cell-range-address", end); writer.startElement("chart:data-point"); writer.addAttribute("chart:repeated", field->rowCount()); writer.endElement(); writer.endElement(); ++field->serieNr; } } else if (tag == "chart:data-point") { // do nothing handled under chart:series } else if (tag == "table:table-header-columns") { writer.startElement(tag.constData()); writer.startElement("table:table-column"); // just an empty tag writer.endElement(); writer.endElement(); } else if (tag == "table:table-columns") { writer.startElement(tag.constData()); writeChartElements(writer, e); writer.endElement(); } else if (tag == "table:table-column") { int columns = m_userfields[m_activefields.last()]->columnCount(); writer.startElement(tag.constData()); writer.addAttribute("table:number-columns-repeated", columns); writer.endElement(); } else if (tag == "table:table-header-rows") { writer.startElement(tag.constData()); writer.startElement("table:table-row"); // first column not used, just an empty cell writer.startElement("table:table-cell"); writer.startElement("text:p"); writer.endElement(); writer.endElement(); // write legends UserField *field = m_userfields[m_activefields.last()]; for (const QString &name : field->columns) { // clazy:exclude=range-loop QString value = field->headerData(name); writer.startElement("table:table-cell"); writer.addAttribute("office:value-type", "string"); writer.startElement("text:p"); writer.addTextNode(value); writer.endElement(); writer.endElement(); } writer.endElement(); writer.endElement(); } else if (tag == "table:table-rows") { writer.startElement(tag.constData()); UserField *field = m_userfields[m_activefields.last()]; int columns = field->columnCount(); if (columns > 0) { int rows = field->model.rowCount(); for (int r = 0; r < rows; ++r) { writer.startElement("table:table-row"); // first x-axis labels QDate date = QDate(field->model.headerData(r, Qt::Vertical, Qt::EditRole).toDate()); // NOTE: 1899-12-30 is the reference date, but could it depend on style? int day = QDate(1899, 12, 30).daysTo(date); writer.startElement("table:table-cell"); writer.addAttribute("office:value", day); writer.addAttribute("office:value-type", "float"); writer.startElement("text:p"); writer.addTextNode(QString::number(day)); writer.endElement(); writer.endElement(); // then the data for (const QString &name : field->columns) { // clazy:exclude=range-loop QVariant value = field->model.index(r, field->column(name)).data(); writer.startElement("table:table-cell"); writer.addAttribute("office:value-type", "float"); writer.addAttribute("office:value", value.toDouble()); writer.startElement("text:p"); writer.addTextNode(QString::number(value.toDouble())); writer.endElement(); writer.endElement(); } writer.endElement(); } } writer.endElement(); } else if (tag == "chart:legend") { writer.startElement(tag.constData()); if (!e.hasAttributeNS(KoXmlNS::chart, "legend-align") && e.hasAttributeNS(KoXmlNS::chart, "legend-position")) { // lowriter does not specify this attribute // If legend-position is start, end, top or bottom // we need to have legend-align == center so that words // repositions the legend correctly QStringList lst = QStringList() << "start" << "end" << "top" << "bottom"; if (lst.contains(e.attributeNS(KoXmlNS::chart, "legend-position"))) { writer.addAttribute("chart:legend-align", "center"); dbgRGChart<<"adding legend-align"; } } writeElementAttributes(writer, e); writeChartElements(writer, e); writer.endElement(); } else { writer.startElement(tag.constData()); writeElementAttributes(writer, e); writeChartElements(writer, e); writer.endElement(); } } } void ReportGeneratorOdt::listChildNodes(const QDomNode &parent) { QDomNodeList lst = parent.childNodes(); for (int i = 0; i < lst.count(); ++i) { if (lst.at(i).isElement()) { QDomElement e = lst.at(i).toElement(); dbgRG<<"Element:"<model.setSourceModel(model); for (int c = 0; c < this->model.columnCount(); ++c) { headerNames << this->model.headerData(c, Qt::Horizontal, role).toString().toLower(); } } int ReportGeneratorOdt::UserField::column(const QString &columnName) const { QStringList l = columnName.split('.'); int c = l.isEmpty() ? -1 : headerNames.indexOf(l.last().toLower()); dbgRGTable<<" column:"< USERFIELD_NONE) { + seqNr = USERFIELD_NONE; + } +} + QString ReportGeneratorOdt::UserField::data(const QString &column) const { QModelIndex idx; if (currentIndex.isValid()) { idx = model.index(currentIndex.row(), this->column(column), currentIndex.parent()); } else { idx = model.index(0, this->column(column)); } QString s = "No data"; if (idx.isValid()) { s = model.data(idx).toString(); } dbgRG< This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef REPORTGENERATORODT_H #define REPORTGENERATORODT_H #include "ReportGenerator.h" #include #include #include #include class QIODevice; class QString; class QBuffer; class KoXmlWriter; class KoStore; class KoOdfWriteStore; class KoOdfReadStore; +#define USERFIELD_TRANSLATION -3 +#define USERFIELD_VARIABLE -2 +#define USERFIELD_NONE -1 +#define USERFIELD_HEADER 0 +#define USERFIELD_DATA 1 + namespace KPlato { class Project; class ScheduleManager; class ItemModelBase; class PLANMODELS_EXPORT ReportGeneratorOdt : public ReportGenerator { public: explicit ReportGeneratorOdt(); ~ReportGeneratorOdt(); bool open(); void close(); bool createReport(); protected: bool createReportOdt(); bool handleTextP(KoXmlWriter &writer, const KoXmlElement &textp); void handleDrawFrame(KoXmlWriter &writer, const KoXmlElement &frame); void treatText(KoXmlWriter &writer, const KoXmlText &text); void treatTable(KoXmlWriter &writer, const KoXmlElement &tableElement); bool treatTableHeaderRows(KoXmlWriter &writer, const KoXmlElement &headerRowElement); bool treatTableRow(KoXmlWriter &writer, const KoXmlElement &rowElement); void treatUserFieldGet(KoXmlWriter &writer, const KoXmlElement &e); void writeElementAttributes(KoXmlWriter &writer, const KoXmlElement &element, const QStringList &exclude = QStringList()); void writeChildElements(KoXmlWriter &writer, const KoXmlElement &parent); bool copyFile(KoStore &from, KoStore &to, const QString &file); KoStore *copyStore(KoOdfReadStore &reader, const QString &outfile); KoXmlWriter *createOasisXmlWriter(KoOdfReadStore &reader, QBuffer *buffer, const QString fileName, const char *rootElementName); void treatEmbededObjects(KoOdfReadStore &reader, KoStore &outStore); void treatChart(KoOdfReadStore &reader, KoStore &outStore, const QString &name, const QString &file); void treatGantt(KoOdfReadStore &reader, KoStore &outStore, const QString &name, const QString &file); void writeChartElements(KoXmlWriter &writer, const KoXmlElement &parent); void listChildNodes(const QDomNode &parent); void handleUserFieldDecls(KoXmlWriter &writer, const KoXmlElement &decls); QAbstractItemModel *dataModel(const QString &name) const; + void addDataModel(const QString &name, QAbstractItemModel *model, int role); + public: class UserField { public: enum Variant { None, Header, Rows, Variable, Translation }; - UserField() : seqNr(-1), serieNr(0) {} + UserField() : seqNr(USERFIELD_NONE), serieNr(0) {} int variant() const; void begin(); bool next(); + void finish(); QString headerData(const QString &columnName) const; QString data(const QString &columnName) const; int rowCount(); int columnCount() const {return columns.count();} void setModel(QAbstractItemModel *model, int role); int column(const QString &columnName) const; QString name; // Name of this field (eg: table1) QString type; // Type of field (eg: table) QString dataName; // Name associated with the data (eg: tasks) QStringList properties; // Info on how to handle the data (eg: values=bcws,bcwp,acwp) QStringList headerNames; // Lowercase list of all header names QStringList columns; // Lowercase list of headernames that shall be used QSortFilterProxyModel model; int seqNr; // A sequence number used to tabulate column names (eg: seqNr=2: table1.name2) int serieNr; // A chart needs to know which data series it works with QString hasLabels; // A chart needs to know if data shall contain legends/labels QModelIndex currentIndex; }; UserField *findUserField(const KoXmlElement &decl) const; private: KoStore *m_templateStore; QStringList m_manifestfiles; QList m_sortedfields; QList m_activefields; QMap m_userfields; QMap m_embededcharts; QMap m_embededgantts; QStringList m_keys; QStringList m_variables; QMap m_datamodels; QMap m_headerrole; QList m_basemodels; QList m_tags; // cache tags for survival }; QDebug operator<<(QDebug dbg, ReportGeneratorOdt::UserField *f); QDebug operator<<(QDebug dbg, ReportGeneratorOdt::UserField &f); } //namespace KPlato #endif // REPORTGENERATOR_H diff --git a/src/libs/ui/CMakeLists.txt b/src/libs/ui/CMakeLists.txt index 9d5a1c5f..7cc2a2e1 100644 --- a/src/libs/ui/CMakeLists.txt +++ b/src/libs/ui/CMakeLists.txt @@ -1,209 +1,213 @@ include_directories( ${PLANKERNEL_INCLUDES} ${PLANMODELS_INCLUDES} ${PLANMAIN_INCLUDES} ${PLANWIDGETS_INCLUDES} ${KDEPIMLIBS_INCLUDE_DIR} ) #add_subdirectory( tests ) ########### KPlato private library ############### if (PLAN_USE_KREPORT) message(STATUS "-- Building plan with reports capability") add_subdirectory(reports/items) set(planreports_LIB_SRC reports/reportview.cpp reports/reportdata.cpp reports/reportsourceeditor.cpp reports/reportscripts.cpp ) set(planreports_ui_LIB_SRCS reports/reportsourceeditor.ui reports/reportnavigator.ui reports/reportsectionswidget.ui reports/reportgroupsectionswidget.ui reports/reporttoolswidget.ui ) endif() set(planui_LIB_SRCS ${planreports_LIB_SRC} TasksEditController.cpp TasksEditDialog.cpp welcome/WelcomeView.cpp reportsgenerator/ReportsGeneratorView.cpp kptganttitemdelegate.cpp kptworkpackagesendpanel.cpp kptworkpackagesenddialog.cpp kptdocumentseditor.cpp kptdocumentspanel.cpp kptdocumentsdialog.cpp kptitemviewsettup.cpp kptsplitterview.cpp kptrelationeditor.cpp kptdependencyeditor.cpp kptusedefforteditor.cpp kpttaskstatusview.cpp kptcalendareditor.cpp kptviewbase.cpp kptaccountseditor.cpp kptperteditor.cpp kptpertresult.cpp kpttaskeditor.cpp kptresourceeditor.cpp kptscheduleeditor.cpp kptsummarytaskdialog.cpp kptsummarytaskgeneralpanel.cpp kptresourceappointmentsview.cpp kptaccountsviewconfigdialog.cpp kptaccountsview.cpp kpttaskcostpanel.cpp kptmilestoneprogresspanel.cpp kptmilestoneprogressdialog.cpp kpttaskdialog.cpp kptmainprojectdialog.cpp kptmainprojectpanel.cpp kptganttview.cpp gantt/DateTimeTimeLine.cpp gantt/DateTimeGrid.cpp kptrelationdialog.cpp kptrequestresourcespanel.cpp kptresourcedialog.cpp kptstandardworktimedialog.cpp kptintervaledit.cpp kpttaskgeneralpanel.cpp kpttaskprogresspanel.cpp kpttaskprogressdialog.cpp kpttaskdescriptiondialog.cpp kptwbsdefinitiondialog.cpp kptwbsdefinitionpanel.cpp kptresourceassignmentview.cpp kptresourceallocationeditor.cpp kptworkpackagemergedialog.cpp kptrecalculatedialog.cpp kpthtmlview.cpp locale/localemon.cpp kptlocaleconfigmoneydialog.cpp ResourceAllocationView.cpp performance/KPlatoChart.cpp performance/PerformanceStatusBase.cpp performance/ProjectStatusView.cpp performance/PerformanceStatusView.cpp performance/PerformanceTableView.cpp ) ki18n_wrap_ui(planui_LIB_SRCS ${planreports_ui_LIB_SRCS} welcome/WelcomeView.ui kptresourceappointmentsdisplayoptions.ui kptganttchartdisplayoptions.ui kptprintingheaderfooter.ui kptganttprintingoptions.ui kptworkpackagesendpanel.ui kptdocumentspanel.ui performance/PerformanceStatus.ui performance/PerformanceStatusViewSettingsPanel.ui kptcpmwidget.ui kptitemviewsettings.ui kptpertresult.ui standardworktimedialogbase.ui kptwbsdefinitionpanelbase.ui kptaccountsviewconfigurepanelbase.ui kptintervaleditbase.ui kpttaskcostpanelbase.ui kpttaskdescriptionpanelbase.ui kptsummarytaskgeneralpanelbase.ui kptmilestoneprogresspanelbase.ui resourcedialogbase.ui kptmainprojectpanelbase.ui relationpanel.ui kpttaskgeneralpanelbase.ui kpttaskprogresspanelbase.ui kptperteditor.ui kptresourceassignmentview.ui kpttaskstatusviewsettingspanel.ui kptworkpackagemergepanel.ui kptrecalculatedialog.ui kptscheduleeditor.ui locale/localemon.ui ) add_library(planui SHARED ${planui_LIB_SRCS}) generate_export_header(planui) target_link_libraries(planui PUBLIC planmain planmodels KF5::KHtml PRIVATE KChart KF5::ItemViews KF5::IconThemes KF5::Archive KF5::TextWidgets -# Qt5::Sql + KF5::KIOCore + KF5::KIOFileWidgets + KF5::KIOWidgets + KF5::KIONTLM ) if (PLAN_USE_KREPORT) target_link_libraries(planui PUBLIC KReport PRIVATE KPropertyWidgets) endif() if(KF5AkonadiContact_FOUND) target_link_libraries(planui PRIVATE KF5::AkonadiContact) endif() set_target_properties(planui PROPERTIES VERSION ${GENERIC_PLAN_LIB_VERSION} SOVERSION ${GENERIC_PLAN_LIB_SOVERSION} ) install(TARGETS planui ${INSTALL_TARGETS_DEFAULT_ARGS}) install( FILES AccountsEditorUi.rc AccountsViewUi.rc CalendarEditorUi.rc TaskEditorUi.rc DependencyEditorUi.rc ResourceEditorUi.rc ResourceAppointmentsViewUi.rc ScheduleEditorUi.rc GanttViewUi.rc WorkPackageViewUi.rc reportsgenerator/ReportsGeneratorViewUi.rc performance/PerformanceStatusViewUi.rc performance/ProjectStatusViewUi.rc PertResultUi.rc TaskViewUi.rc TaskStatusViewUi.rc DESTINATION ${KXMLGUI_INSTALL_DIR}/calligraplan ) # reports files install(FILES + reportsgenerator/templates/EmptyTemplate.odt reportsgenerator/ProjectPerformanceCost.odt reportsgenerator/ProjectPerformance.odt reportsgenerator/TaskStatus.odt DESTINATION ${DATA_INSTALL_DIR}/calligraplan/reports ) diff --git a/src/libs/ui/reportsgenerator/ProjectPerformance.odt b/src/libs/ui/reportsgenerator/ProjectPerformance.odt index 159d6e12..8d08bcdf 100644 Binary files a/src/libs/ui/reportsgenerator/ProjectPerformance.odt and b/src/libs/ui/reportsgenerator/ProjectPerformance.odt differ diff --git a/src/libs/ui/reportsgenerator/ProjectPerformanceCost.odt b/src/libs/ui/reportsgenerator/ProjectPerformanceCost.odt index c4c87c5e..50fc0a83 100644 Binary files a/src/libs/ui/reportsgenerator/ProjectPerformanceCost.odt and b/src/libs/ui/reportsgenerator/ProjectPerformanceCost.odt differ diff --git a/src/libs/ui/reportsgenerator/ReportsGeneratorView.cpp b/src/libs/ui/reportsgenerator/ReportsGeneratorView.cpp index 2480ea46..0852b89d 100644 --- a/src/libs/ui/reportsgenerator/ReportsGeneratorView.cpp +++ b/src/libs/ui/reportsgenerator/ReportsGeneratorView.cpp @@ -1,531 +1,535 @@ /* This file is part of the KDE project Copyright (C) 2017 Dag Andersen This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // clazy:excludeall=qstring-arg #include "ReportsGeneratorView.h" #include "reportgenerator/ReportGenerator.h" #include "Help.h" #include "kptdebug.h" #include #include +#include #include #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KPlato { #define FULLPATHROLE Qt::UserRole + 123 class TemplateFileDelegate : public QStyledItemDelegate { public: TemplateFileDelegate(QObject *parent); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; QMap files; }; TemplateFileDelegate::TemplateFileDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QWidget *TemplateFileDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option) Q_UNUSED(index); return new QComboBox(parent); } void TemplateFileDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QComboBox *cb = qobject_cast(editor); debugPlan<setEditable(true); cb->addItems(files.keys()); QString file = index.data().toString(); cb->setCurrentText(file); } void TemplateFileDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QComboBox *cb = qobject_cast(editor); debugPlan<currentText(); debugPlan<<"template file:"<setData(index, nfile); if (files.contains(nfile)) { nfile = files[nfile].url(); } model->setData(index, nfile, FULLPATHROLE); } } else debugPlan<<" No combo box editor!!"; } class FileItemDelegate : public QStyledItemDelegate { public: FileItemDelegate(QObject *parent); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; QMap files; }; FileItemDelegate::FileItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QWidget *FileItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); KUrlRequester *u = new KUrlRequester(parent); u->setMode(KFile::File); return u; } void FileItemDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { KUrlRequester *u = qobject_cast(editor); QString file = index.data().toString(); if (!file.isEmpty()) { u->setUrl(QUrl(file)); } } void FileItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { KUrlRequester *u = qobject_cast(editor); if (u && index.isValid()) { model->setData(index, u->url().url()); } } class FileNameExtensionDelegate : public QStyledItemDelegate { public: FileNameExtensionDelegate(QObject *parent); QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const; void setEditorData(QWidget *editor, const QModelIndex &index) const; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const; }; FileNameExtensionDelegate::FileNameExtensionDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QWidget *FileNameExtensionDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { Q_UNUSED(option); Q_UNUSED(index); QComboBox *cb = new QComboBox(parent); for (int i = 0; i < ReportsGeneratorView::addOptions().count(); ++i) { cb->addItem(ReportsGeneratorView::addOptions().at(i), ReportsGeneratorView::addTags().value(i)); } return cb; } void FileNameExtensionDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { QComboBox *cb = qobject_cast(editor); if (cb) { int idx = ReportsGeneratorView::addTags().indexOf(index.data(Qt::UserRole).toString()); cb->setCurrentIndex(idx < 0 ? 0 : idx); } } void FileNameExtensionDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QComboBox *cb = qobject_cast(editor); if (cb && index.isValid()) { model->setData(index, cb->currentData(), Qt::UserRole); model->setData(index, cb->currentText()); } } QStringList ReportsGeneratorView::addOptions() { return QStringList() << i18n("Nothing") << i18n("Date") << i18n("Number"); } QStringList ReportsGeneratorView::addTags() { return QStringList() << "Nothing" << "Date" << "Number"; } ReportsGeneratorView::ReportsGeneratorView(KoPart *part, KoDocument *doc, QWidget *parent) : ViewBase(part, doc, parent) { debugPlan<<"----------------- Create ReportsGeneratorView ----------------------"; setXMLFile("ReportsGeneratorViewUi.rc"); QVBoxLayout * l = new QVBoxLayout(this); l->setMargin(0); m_view = new QTreeView(this); QStandardItemModel *m = new QStandardItemModel(m_view); m->setHorizontalHeaderLabels(QStringList() << i18n("Name") << i18n("Report Template") << i18n("Report File") << i18n("Add")); m->setHeaderData(0, Qt::Horizontal, xi18nc("@info:tooltip", "Report name"), Qt::ToolTipRole); m->setHeaderData(1, Qt::Horizontal, xi18nc("@info:tooltip", "Report template file name"), Qt::ToolTipRole); m->setHeaderData(2, Qt::Horizontal, xi18nc("@info:tooltip", "Name of the generated report file"), Qt::ToolTipRole); m->setHeaderData(3, Qt::Horizontal, xi18nc("@info:tooltip", "Information added to filename"), Qt::ToolTipRole); m_view->setModel(m); m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setRootIsDecorated(false); m_view->setAlternatingRowColors(true); connect(m_view, &QWidget::customContextMenuRequested, this, &ReportsGeneratorView::slotContextMenuRequested); l->addWidget(m_view); TemplateFileDelegate *del = new TemplateFileDelegate(m_view); QString path = QStandardPaths::locate(QStandardPaths::AppDataLocation, "reports", QStandardPaths::LocateDirectory); debugPlan<<"standardpath:"<files.insert(url.fileName(), url); } } m_view->setItemDelegateForColumn(1, del); m_view->setItemDelegateForColumn(2, new FileItemDelegate(m_view)); m_view->setItemDelegateForColumn(3, new FileNameExtensionDelegate(m_view)); m_view->header()->setSectionResizeMode(3, QHeaderView::Fixed); m_view->header()->resizeSection(3, 12); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ReportsGeneratorView::slotSelectionChanged); setupGui(); Help::add(this, xi18nc("@info:whatsthis", "Add and generate reports" "" "Enables you to add and generate reports based on Open Document (.odf) files." "" "You can create a report template using any Open Document text editor." "More..." "", Help::page("Manual/Reports_Generator_View"))); } void ReportsGeneratorView::setGuiActive(bool activate) { debugPlan<selectionModel()->selectedRows(); } int ReportsGeneratorView::selectedRowCount() const { return selectedRows().count(); } void ReportsGeneratorView::slotContextMenuRequested(const QPoint& pos) { debugPlan; emit requestPopupMenu("reportsgeneratorview_popup", m_view->mapToGlobal(pos)); } void ReportsGeneratorView::slotEnableActions() { updateActionsEnabled(isReadWrite()); } void ReportsGeneratorView::updateActionsEnabled(bool on) { actionAddReport->setEnabled(on); actionRemoveReport->setEnabled(on && selectedRowCount() > 0); actionGenerateReport->setEnabled(on && selectedRowCount() > 0); } void ReportsGeneratorView::setupGui() { KActionCollection *coll = actionCollection(); actionAddReport = new QAction(koIcon("list-add"), i18n("Add Report"), this); coll->addAction("add_report", actionAddReport); coll->setDefaultShortcut(actionAddReport, Qt::CTRL + Qt::Key_I); connect(actionAddReport, &QAction::triggered, this, &ReportsGeneratorView::slotAddReport); addContextAction(actionAddReport); actionRemoveReport = new QAction(koIcon("list-remove"), i18n("Remove Report"), this); coll->addAction("remove_report", actionRemoveReport); coll->setDefaultShortcut(actionRemoveReport, Qt::CTRL + Qt::Key_D); connect(actionRemoveReport, &QAction::triggered, this, &ReportsGeneratorView::slotRemoveReport); addContextAction(actionRemoveReport); actionGenerateReport = new QAction(koIcon("document-export"), i18n("Generate Report"), this); coll->addAction("generate_report", actionGenerateReport); coll->setDefaultShortcut(actionGenerateReport, Qt::CTRL + Qt::Key_G); connect(actionGenerateReport, &QAction::triggered, this, &ReportsGeneratorView::slotGenerateReport); addContextAction(actionGenerateReport); // createOptionAction(); } void ReportsGeneratorView::slotOptions() { debugPlan; // SplitItemViewSettupDialog *dlg = new SplitItemViewSettupDialog(this, m_view, this); // dlg->addPrintingOptions(); // connect(dlg, SIGNAL(finished(int)), SLOT(slotOptionsFinished(int))); // dlg->show(); // dlg->raise(); // dlg->activateWindow(); } void ReportsGeneratorView::slotAddReport() { debugPlan; QAbstractItemModel *m = m_view->model(); int row = m->rowCount(); m->insertRow(row); QModelIndex idx = m->index(row, 0); m->setData(idx, i18n("New report")); QModelIndex add = m->index(row, 3); m->setData(add, ReportsGeneratorView::addOptions().at(0)); m->setData(add, ReportsGeneratorView::addTags().at(0), Qt::UserRole); m_view->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); m_view->edit(idx); emit optionsModified(); } void ReportsGeneratorView::slotRemoveReport() { debugPlan<model(); QModelIndexList lst = selectedRows(); if (lst.isEmpty()) { return; } // Assumption: model is flat // We must do this in descending row order QMap map; for (int i = 0; i < lst.count(); ++i) { map.insert(-lst.at(i).row(), lst.at(i)); // sort descending } for (const QModelIndex &idx : map) { Q_ASSERT(!idx.parent().isValid()); // must be flat m->removeRow(idx.row(), idx.parent()); } emit optionsModified(); } void ReportsGeneratorView::slotGenerateReport() { debugPlan; QAbstractItemModel *model = m_view->model(); foreach(const QModelIndex &idx, selectedRows()) { QString name = model->index(idx.row(), 0).data().toString(); QString tmp = model->index(idx.row(), 1).data(FULLPATHROLE).toString(); QString file = model->index(idx.row(), 2).data().toString(); if (tmp.isEmpty()) { QMessageBox::information(this, xi18nc("@title:window", "Generate Report"), i18n("Failed to generate %1." "\nTemplate file name is empty.", name)); continue; } if (file.isEmpty()) { debugPlan<<"No files for report:"<index(idx.row(), 3).data(Qt::UserRole).toString(); if (addition == "Date") { int dotpos = file.lastIndexOf('.'); QString date = QDate::currentDate().toString(); file = file.insert(dotpos, date.prepend('-')); } else if (addition == "Number") { int dotpos = file.lastIndexOf('.'); QString fn = file; for (int i = 1; QFile::exists(fn); ++i) { fn = file.insert(dotpos, QString::number(i).prepend('-')); } file = fn; } // warn if file exists if (QFile::exists(QUrl(file).path())) { if (QMessageBox::question(this, i18n("Report Generation"), i18n("File exists. Continue?")) == QMessageBox::No) { return; } } generateReport(tmp, file); } } bool ReportsGeneratorView::generateReport(const QString &templateFile, const QString &file) { ReportGenerator rg; rg.setReportType("odt"); // TODO: handle different report types rg.setTemplateFile(templateFile); rg.setReportFile(file); rg.setProject(project()); rg.setScheduleManager(scheduleManager()); if (!rg.open()) { debugPlan<<"Failed to open report generator"; QMessageBox::warning(this, i18n("Failed to open report generator"), rg.lastError()); return false; } if (!rg.createReport()) { QMessageBox::warning(this, i18n("Failed to create report"), rg.lastError()); return false; } - QMessageBox::information(this, i18n("Report Generation"), i18n("Report file generated: %1", file)); + if (QMessageBox::question(this, i18nc("@title", "Report Generation"), i18nc("@info", "Report file generated:%1", file), QMessageBox::Open|QMessageBox::Close, QMessageBox::Close) == QMessageBox::Open) { + return KRun::runUrl(QUrl(file), "application/vnd.oasis.opendocument.text", window(), (KRun::RunFlags)0); + } return true; } bool ReportsGeneratorView::loadContext(const KoXmlElement &context) { debugPlan; m_view->header()->setStretchLastSection((bool)(context.attribute("stretch-last-column", "1").toInt())); KoXmlElement e = context.namedItem("sections").toElement(); if (!e.isNull()) { QHeaderView *h = m_view->header(); QString s("section-%1"); for (int i = 0; i < h->count(); ++i) { if (e.hasAttribute(s.arg(i))) { int index = e.attribute(s.arg(i), "-1").toInt(); if (index >= 0 && index < h->count()) { h->moveSection(h->visualIndex(index), i); } } } } KoXmlElement parent = context.namedItem("data").toElement(); if (!parent.isNull()) { debugPlan<<"Load data"; int row = 0; QAbstractItemModel *model = m_view->model(); forEachElement(e, parent) { if (e.tagName() != "row") { continue; } model->insertRow(row); QString name = e.attribute("name"); QString tmp = e.attribute("template"); QString file = e.attribute("file"); QString add = e.attribute("add"); QModelIndex idx = model->index(row, 0); model->setData(idx, name); idx = model->index(row, 1); model->setData(idx, tmp, FULLPATHROLE); model->setData(idx, QUrl(tmp).fileName()); idx = model->index(row, 2); model->setData(idx, file); idx = model->index(row, 3); model->setData(idx, add, Qt::UserRole); model->setData(idx, ReportsGeneratorView::addOptions().value(ReportsGeneratorView::addTags().indexOf(add))); ++row; } } ViewBase::loadContext(context); for (int c = 0; c < m_view->header()->count(); ++c) { m_view->resizeColumnToContents(c); } return true; } void ReportsGeneratorView::saveContext(QDomElement &context) const { debugPlan; context.setAttribute( "stretch-last-column", QString::number(m_view->header()->stretchLastSection()) ); QDomElement e = context.ownerDocument().createElement("sections"); context.appendChild(e); QHeaderView *h = m_view->header(); for (int i = 0; i < h->count(); ++i) { e.setAttribute(QString("section-%1").arg(i), h->logicalIndex(i)); } QDomElement data = context.ownerDocument().createElement("data"); context.appendChild(data); const QAbstractItemModel *model = m_view->model(); for (int row = 0; row < model->rowCount(); ++row) { e = data.ownerDocument().createElement("row"); data.appendChild(e); QModelIndex idx = model->index(row, 0); e.setAttribute("name", idx.data().toString()); idx = model->index(row, 1); e.setAttribute("template", idx.data(FULLPATHROLE).toString()); idx = model->index(row, 2); e.setAttribute("file", idx.data().toString()); idx = model->index(row, 3); e.setAttribute("add", idx.data(Qt::UserRole).toString()); } ViewBase::saveContext(context); } } // namespace KPlato diff --git a/src/libs/ui/reportsgenerator/TaskStatus.odt b/src/libs/ui/reportsgenerator/TaskStatus.odt index aed0e5d1..31fe6e57 100644 Binary files a/src/libs/ui/reportsgenerator/TaskStatus.odt and b/src/libs/ui/reportsgenerator/TaskStatus.odt differ diff --git a/src/libs/ui/reportsgenerator/templates/EmptyTemplate.odt b/src/libs/ui/reportsgenerator/templates/EmptyTemplate.odt new file mode 100644 index 00000000..8e8d33c4 Binary files /dev/null and b/src/libs/ui/reportsgenerator/templates/EmptyTemplate.odt differ