diff --git a/src/planpart.desktop b/src/planpart.desktop index 4d8a8b9d..b03d0039 100644 --- a/src/planpart.desktop +++ b/src/planpart.desktop @@ -1,27 +1,27 @@ [Desktop Entry] Name=Calligra Project Management Component Name[ca]=Component de gestió de projectes del Calligra Name[ca@valencia]=Component de gestió de projectes del Calligra Name[cs]=Komponenta Calligra pro správu projektů Name[de]=Calligra-Komponente für Projektmanagement Name[en_GB]=Calligra Project Management Component Name[es]=Componente de gestión de proyectos de Calligra Name[fr]=Composant gestion de projets de Calligra Name[gl]=Compoñente para Calligra de xestión de proxectos Name[it]=Componente per la gestione dei progetti di Calligra Name[nl]=Calligra Projectbeheercomponent Name[pl]=Składnik zarządzania projektami dla Calligry Name[pt]=Componente de Gestão de Projectos do Calligra Name[pt_BR]=Componente de gerenciamento de projetos do Calligra Name[ru]=Компонент управления проектами Calligra Name[sk]=Calligra modul pre správu projektov Name[sv]=Calligra projekthanteringskomponent Name[uk]=Компонент керування проектами Calligra Name[x-test]=xxCalligra Project Management Componentxx Name[zh_CN]=Calligra 项目管理组件 X-KDE-Library=planpart -MimeType=application/x-vnd.kde.plan;application/x-vnd.kde.kplato; +MimeType=application/x-vnd.kde.plan;application/x-vnd.kde.kplato;application/x-planner Type=Service X-KDE-ServiceTypes=Calligra/Part X-KDE-NativeMimeType=application/x-vnd.kde.plan Icon=calligraplan diff --git a/src/plugins/filters/CMakeLists.txt b/src/plugins/filters/CMakeLists.txt index c5df44ab..3082be74 100644 --- a/src/plugins/filters/CMakeLists.txt +++ b/src/plugins/filters/CMakeLists.txt @@ -1,9 +1,10 @@ add_subdirectory( icalendar ) +add_subdirectory( planner ) if(WIN32) #disable for now #add_subdirectory( kplato ) else() add_subdirectory( kplato ) endif() diff --git a/src/plugins/filters/planner/CMakeLists.txt b/src/plugins/filters/planner/CMakeLists.txt new file mode 100644 index 00000000..6021641d --- /dev/null +++ b/src/plugins/filters/planner/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory( import ) diff --git a/src/plugins/filters/planner/import/CMakeLists.txt b/src/plugins/filters/planner/import/CMakeLists.txt new file mode 100644 index 00000000..e8cd91be --- /dev/null +++ b/src/plugins/filters/planner/import/CMakeLists.txt @@ -0,0 +1,24 @@ + +include_directories( + ${PLAN_SOURCE_DIR} + ${PLANMAIN_INCLUDES} +) + +set(plannerimport_PART_SRCS + plannerimport.cpp +) + + +add_library(planplannerimport MODULE ${plannerimport_PART_SRCS}) +# calligraplan_filter_desktop_to_json(planplannerimport plan_planner_import.desktop) +if(${KF5_VERSION} VERSION_LESS "5.16.0") + kcoreaddons_desktop_to_json(planplannerimport plan_planner_import.desktop) +else() + kcoreaddons_desktop_to_json(planplannerimport plan_planner_import.desktop + SERVICE_TYPES ${PLAN_SOURCE_DIR}/servicetypes/calligraplan_filter.desktop + ) +endif() + +target_link_libraries(planplannerimport planprivate plankernel planmain) + +install(TARGETS planplannerimport DESTINATION ${PLUGIN_INSTALL_DIR}/calligraplan/formatfilters) diff --git a/src/plugins/filters/planner/import/plan_planner_import.desktop b/src/plugins/filters/planner/import/plan_planner_import.desktop new file mode 100644 index 00000000..8e2dbbb2 --- /dev/null +++ b/src/plugins/filters/planner/import/plan_planner_import.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Service +Name=Plan Planner Import Filter +Name[x-test]=xxPlan Planner Import Filterxx +X-KDE-Export=application/x-vnd.kde.plan +X-KDE-Import=application/x-planner +X-KDE-Weight=1 +X-KDE-Library=planplannerimport +X-KDE-ServiceTypes=Plan/Filter diff --git a/src/plugins/filters/planner/import/plannerimport.cpp b/src/plugins/filters/planner/import/plannerimport.cpp new file mode 100644 index 00000000..fc5efb9f --- /dev/null +++ b/src/plugins/filters/planner/import/plannerimport.cpp @@ -0,0 +1,552 @@ +/* This file is part of the KDE project + * Copyright (C) 2019 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 "plannerimport.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + + +using namespace KPlato; + +#define PLANNERIMPORT_LOG "calligra.plan.filter.planner.import" +#define debugPlannerImport qCDebug(QLoggingCategory(PLANNERIMPORT_LOG))<();) + +PlannerImport::PlannerImport(QObject* parent, const QVariantList &) + : KoFilter(parent) +{ +} + +KoFilter::ConversionStatus PlannerImport::convert(const QByteArray& from, const QByteArray& to) +{ + debugPlannerImport << from << to; + if ((from != "application/x-planner") || (to != "application/x-vnd.kde.plan")) { + return KoFilter::NotImplemented; + } + QFile in(m_chain->inputFile()); + if (!in.open(QIODevice::ReadOnly)) { + errorPlannerImport << "Unable to open input file!"; + in.close(); + return KoFilter::FileNotFound; + } + QDomDocument inDoc; + if (!inDoc.setContent(&in)) { + errorPlannerImport << "Invalid format in input file!"; + in.close(); + return KoFilter::InvalidFormat; + } + + MainDocument *part = 0; + bool batch = false; + if (m_chain->manager()) { + batch = m_chain->manager()->getBatchMode(); + } + if (batch) { + //TODO + debugPlannerImport << "batch"; + } else { + //debugPlannerImport<<"online"; + part = qobject_cast(m_chain->outputDocument()); + } + if (part == 0) { + errorPlannerImport << "Cannot open document"; + return KoFilter::InternalError; + } + if (!loadPlanner(inDoc, part)) { + return KoFilter::ParsingError; + } + + return KoFilter::OK; +} + +DateTime toDateTime(const QString &dts) +{ + // NOTE: time ends in Z, so should be UTC, but it seems it is in local time anyway. + // Atm, just ignore timezone + const QString format = QString("yyyyMMddThhmmssZ"); + return DateTime(QDateTime::fromString(dts, format)); +} + +// +bool loadProject(const QDomElement &el, Project &project) +{ + ScheduleManager *sm = project.createScheduleManager("Planner"); + project.addScheduleManager(sm); + sm->createSchedules(); + sm->setAllowOverbooking(true); + sm->expected()->setScheduled(true); + project.setCurrentSchedule(sm->scheduleId()); + + project.setName(el.attribute("name")); + project.setLeader(el.attribute("manager")); + DateTime dt = toDateTime(el.attribute("project-start")); + if (dt.isValid()) { + project.setConstraintStartTime(dt); + project.setStartTime(dt); + } + if (el.hasAttribute("calendar")) { + Calendar *c = new Calendar(); + c->setId(el.attribute("calendar")); + project.addCalendar(c); + project.setDefaultCalendar(c); + debugPlannerImport<<"Added default calendar:"< +// +// +// +// +// +// +// +// +// +// +// +// interval start="0900" end="1700"/> +// /overridden-day-type> +// /overridden-day-types> +// days/> +// /calendar> +// calendar id="2" name="Standard"> +// default-week mon="0" tue="0" wed="0" thu="0" fri="0" sat="1" sun="1"/> +// overridden-day-types> +// overridden-day-type id="0"> +// interval start="0800" end="1200"/> +// interval start="1300" end="1700"/> +// /overridden-day-type> +// /overridden-day-types> +// days/> +// calendar id="3" name="Min kalender"> +// +// overridden-day-types> +// overridden-day-type id="3"> +// interval start="0800" end="1600"/> +// /overridden-day-type> +// /overridden-day-types> +// days> +// day date="20190506" type="day-type" id="3"/> +// /days> +// /calendar> +// /calendar> +// +CalendarDay::State toDayState(int type) +{ + + QList state = QList() << CalendarDay::Working << CalendarDay::NonWorking; + if (type < state.count()) { + return state.value(type); + } + return CalendarDay::Undefined; +} +bool loadWeek(const QDomElement &el, Calendar *calendar) +{ + debugPlannerImport<name(); + QList defaultWeek = QList() << 2 << 2 << 2 << 2 << 2 << 2 << 2; + QDomElement wel; + forEachChildElementWithTag(wel, el, "default-week") { + defaultWeek[0] = wel.attribute("mon", "2").toInt(); + defaultWeek[1] = wel.attribute("tue", "2").toInt(); + defaultWeek[2] = wel.attribute("wed", "2").toInt(); + defaultWeek[3] = wel.attribute("thu", "2").toInt(); + defaultWeek[4] = wel.attribute("fri", "2").toInt(); + defaultWeek[5] = wel.attribute("sat", "2").toInt(); + defaultWeek[6] = wel.attribute("sun", "2").toInt(); + } + debugPlannerImport<weekday(i+1); + day->setState(toDayState(defaultWeek.at(i))); + } + forEachChildElementWithTag(wel, el, "overridden-day-types") { + QDomElement oel; + forEachChildElementWithTag(oel, wel, "overridden-day-type") { + if (oel.hasAttribute("id")) { + int id = oel.attribute("id").toInt(); + if (!defaultWeek.contains(id)) { + continue; + } + for (int i = 0; i < defaultWeek.count(); ++i) { + if (defaultWeek.at(i) != id) { + continue; + } + CalendarDay *day = calendar->weekday(i+1); + day->setState(CalendarDay::Working); + QDomElement iel; + forEachChildElementWithTag(iel, oel, "interval") { + QTime start = QTime::fromString(iel.attribute("start"), "hhmm"); + QTime end = QTime::fromString(iel.attribute("end"), "hhmm"); + day->addInterval(TimeInterval(start, start.msecsTo(end))); + debugPlannerImport<<"Overriden:"<"<addInterval(TimeInterval(start, start.msecsTo(end))); + } + calendar->addDay(day); + } + return true; +} +bool loadCalendars(const QDomElement &el, Project &project, Calendar *parent = 0) +{ + QDomElement cel; + forEachChildElementWithTag(cel, el, "calendar") { + QString id = cel.attribute("id"); + Calendar *calendar = project.findCalendar(id); + if (!calendar) { + calendar = new Calendar(); + calendar->setId(cel.attribute("id")); + project.addCalendar(calendar, parent); + debugPlannerImport<<"Loading new calendar"<id(); + } else debugPlannerImport<<"Loading default calendar"<id(); + calendar->setName(cel.attribute("name")); + loadWeek(cel, calendar); + loadDays(cel, calendar); + + loadCalendars(cel, project, calendar); + } + return true; +} + +// +// +// +bool loadResourceGroups(const QDomElement &el, Project &project) +{ + QDomNodeList lst = el.elementsByTagName("group"); + QDomElement gel; + forEachElementInList(gel, lst) { + ResourceGroup *g = new ResourceGroup(); + g->setId(gel.attribute("id")); + g->setName(gel.attribute("name")); + project.addResourceGroup(g); + } + return true; +} + +// +// +// +Resource::Type toResourceType(const QString &type) +{ + QMap types; + types["0"] = Resource::Type_Material; + types["1"] = Resource::Type_Work; + return types.contains(type) ? types[type] : Resource::Type_Work; +} + +bool loadResources(const QDomElement &el, Project &project) +{ + QDomNodeList lst = el.elementsByTagName("resource"); + QDomElement rel; + forEachElementInList(rel, lst) { + Resource *r = new Resource(); + r->setId(rel.attribute("id")); + r->setName(rel.attribute("name")); + r->setInitials(rel.attribute("short-name")); + r->setEmail(rel.attribute("email")); + r->setType(toResourceType(rel.attribute("type"))); + int units = rel.attribute("units", "0").toInt(); + if (units == 0) { + // atm. planner saves 0 but assumes 100% + units = 100; + } + r->setUnits(units); + r->setNormalRate(rel.attribute("std-rate").toDouble()); + r->setCalendar(project.findCalendar(rel.attribute("calendar"))); + QString gid = rel.attribute("group"); + ResourceGroup *g = project.group(gid); + if (!g) { + // add a default + g = new ResourceGroup(); + g->setId(gid); // FIXME handle: gid *should* be empty if group was not found + g->setName(i18n("Resources")); + project.addResourceGroup(g); + } + project.addResource(g, r); + } + return true; +} + +Estimate::Type toEstimateType(const QString type) +{ + Estimate::Type res = Estimate::Type_Effort; + if (type == "fixed-work") { + res = Estimate::Type_Effort; + } else if (type == "fixed-duration") { + res = Estimate::Type_Duration; + } + return res; +} + +Node::ConstraintType toConstraintType(const QString &type) +{ + Node::ConstraintType res = Node::ASAP; + if (type == "must-start-on") { + res = Node::MustStartOn; + } else if (type == "start-no-earlier-than") { + res = Node::StartNotEarlier; + } + return res; +} +bool loadConstraint(const QDomElement &el, Task *t) +{ + QDomElement cel; + forEachChildElementWithTag(cel, el, "constraint") { + t->setConstraint(toConstraintType(cel.attribute("type"))); + t->setConstraintStartTime(toDateTime(cel.attribute("time"))); + } + return true; +} +// +bool loadTasks(const QDomElement &el, Project &project, Node *parent = 0) +{ + QDomElement cel; + forEachChildElementWithTag(cel, el, "task") { + Task *t = project.createTask(); + t->setId(cel.attribute("id", t->id())); + t->setName(cel.attribute("name")); + t->setDescription(cel.attribute("note")); + loadConstraint(cel, t); + t->estimate()->setType(toEstimateType(cel.attribute("scheduling"))); + t->estimate()->setExpectedEstimate(Duration(cel.attribute("work", "0").toDouble(), Duration::Unit_s).toDouble()); + + project.addSubTask(t, parent); + long sid = project.scheduleManagers().first()->scheduleId(); + NodeSchedule *sch = new NodeSchedule(); + sch->setId(sid); + sch->setNode(t); + t->addSchedule(sch); + sch->setParent(t->parentNode()->currentSchedule()); + t->setCurrentSchedule(sid); + + const QString format = QString("yyyyMMddThhmmssZ"); + QDateTime start = QDateTime::fromString(cel.attribute("work-start"), format); + QDateTime end = QDateTime::fromString(cel.attribute("end"), format); + t->setStartTime(DateTime(start)); + t->setEndTime(DateTime(end)); + sch->setScheduled(true); + + debugPlannerImport<<"Loaded:"< types; + types["FS"] = Relation::FinishStart; + types["FF"] = Relation::FinishFinish; + types["SS"] = Relation::StartStart; + types["SF"] = Relation::FinishStart; // not supported, use default + + return types.value(type); +} + +// +// +// +bool loadDependencies(const QDomElement &el, Project &project) +{ + QDomElement cel; + forEachChildElementWithTag(cel, el, "task") { + QString succid = cel.attribute("id"); + Node *child = project.findNode(succid); + if (!child) { + warnPlannerImport<<"Task"< +bool loadAllocations(const QDomElement &el, Project &project) +{ + QDomNodeList lst = el.elementsByTagName("allocation"); + QDomElement pel; + forEachElementInList(pel, lst) { + Task *t = dynamic_cast(project.findNode(pel.attribute("task-id"))); + Resource *r = project.findResource(pel.attribute("resource-id")); + if (!t || !r) { + warnPlannerImport<<"Could not find task/resource:"<resourceGroupRequest(r->parentGroup()); + if (!gr) { + gr = new ResourceGroupRequest(r->parentGroup()); + t->addRequest(gr); + } + ResourceRequest *rr = new ResourceRequest(r); + rr->setUnits(pel.attribute("units").toInt()); + gr->addResourceRequest(rr); + + // do assignments + Calendar *calendar = r->calendar(); + if (!calendar) { + warnPlannerImport<<"No resource calendar:"<currentSchedule(); + Schedule *rs = r->schedule(ts->id()); + if (!rs) { + rs = r->createSchedule(t->name(), t->type(), ts->id()); + } + r->setCurrentSchedulePtr(rs); + AppointmentIntervalList apps = calendar->workIntervals(t->startTime(), t->endTime(), rr->units()); + foreach (const AppointmentInterval &a, apps.map()) { + r->addAppointment(ts, a.startTime(), a.endTime(), a.load()); + } + rs->setScheduled(true); + debugPlannerImport<<"Assignments:"<appointmentIntervals().intervals(); + } + return true; +} + +bool PlannerImport::loadPlanner(const QDomDocument &in, MainDocument *doc) const +{ + QDomElement pel = in.documentElement(); + if (pel.tagName() != "project") { + errorPlannerImport << "Missing project element"; + return false; + } + Project &project = doc->getProject(); + if (!loadProject(pel, project)) { + return false; + } + QDomElement el = pel.elementsByTagName("calendars").item(0).toElement(); + if (el.isNull()) { + debugPlannerImport << "No calendars element"; + } + loadCalendars(el, project); + + el = pel.elementsByTagName("resource-groups").item(0).toElement(); + if (el.isNull()) { + debugPlannerImport << "No resource-groups element"; + } + loadResourceGroups(el, project); + + el = pel.elementsByTagName("resources").item(0).toElement(); + if (el.isNull()) { + debugPlannerImport << "No resources element"; + } + loadResources(el, project); + + el = pel.elementsByTagName("tasks").item(0).toElement(); + if (el.isNull()) { + debugPlannerImport << "No tasks element"; + } else { + loadTasks(el, project); + loadDependencies(el, project); + } + loadAllocations(pel, project); + + foreach(const Node *n, project.allNodes()) { + if (n->endTime() > project.endTime()) { + project.setEndTime(n->endTime()); + } + } + return true; +} + +#include "plannerimport.moc" diff --git a/src/plugins/filters/planner/import/plannerimport.h b/src/plugins/filters/planner/import/plannerimport.h new file mode 100644 index 00000000..143db702 --- /dev/null +++ b/src/plugins/filters/planner/import/plannerimport.h @@ -0,0 +1,49 @@ +/* This file is part of the KDE project + * Copyright (C) 2019 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. + */ + +#ifndef PLANNERIMPORT_H +#define PLANNERIMPORT_H + + +#include + + +#include +#include + +class QByteArray; +class QDomDocument; + +namespace KPlato { + class MainDocument; +} + +class PlannerImport : public KoFilter +{ + Q_OBJECT +public: + PlannerImport(QObject* parent, const QVariantList &); + virtual ~PlannerImport() {} + + virtual KoFilter::ConversionStatus convert(const QByteArray& from, const QByteArray& to); + + bool loadPlanner(const QDomDocument &in, KPlato::MainDocument *doc) const; +}; + +#endif // PLANNERIMPORT_H