diff --git a/CMakeLists.txt b/CMakeLists.txt index c231dfdcb..6440cc257 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,7 @@ include(cmake/fetch_kdeaddons.cmake) option(PACKAGE_TESTS "Build the tests" ON) if(PACKAGE_TESTS) message("Add sub directory tests") + enable_testing() add_subdirectory(tests) endif() diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 3d44ef907..3fdb11e08 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -177,8 +177,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/ui/wiringlistdialog.h ${QET_DIR}/sources/ui/wiringlistdialog.cpp ${QET_DIR}/sources/conductornumexport.h - ${QET_DIR}/sources/devicebomexport.cpp - ${QET_DIR}/sources/devicebomexport.h + ${QET_DIR}/sources/bomexport.cpp + ${QET_DIR}/sources/bomexport.h ${QET_DIR}/sources/conductorprofile.cpp ${QET_DIR}/sources/conductorprofile.h ${QET_DIR}/sources/conductorproperties.cpp diff --git a/docs/smart-device-bom.md b/docs/smart-device-bom.md index 583f2ce74..d62823919 100644 --- a/docs/smart-device-bom.md +++ b/docs/smart-device-bom.md @@ -2,135 +2,92 @@ ## Architecture -- A `.qet` project is XML, rooted at `project`. `QETProject::toXml()` and - `write()` serialize it; the file constructor, `readProjectXml()` and - `readDiagramsXml()` load it (`sources/qetproject.cpp`). Diagrams are - serialized by `Diagram::toXml()/fromXml()` (`sources/diagram.cpp`). -- `.elmt` symbol definitions are stored in collections and embedded into a - project by `XmlElementCollection`. `Element::buildFromXml()` builds their - graphical representation (`sources/qetgraphicsitem/element.cpp`). -- A placed component is an `Element`. `ElementData` holds its type and - `m_informations`, a `DiagramContext` key/value map. Instance properties are - read/written by `Element::fromXml()/toXml()` as `elementInformations` with - `elementInformation name="..."` children. -- `ElementPropertiesWidget` hosts `ElementInfoWidget`; the latter creates - rows from `QETInformation::elementInfoKeys()`. Edits use - `ChangeElementInformationCommand`, including live editing and undo/redo. -- Existing reporting uses the derived SQLite `projectDataBase`, - `element_nomenclature_view`, `ElementQueryWidget`, `BOMExportDialog` and - `QetGraphicsTableFactory`. `cli_export.cpp` also provides `--export-bom`. - The database is not the authoritative project storage. +- A `.qet` project is XML. Placed component properties are held in + `ElementData::m_informations`, a `DiagramContext`, and are serialized by the + existing `Element::fromXml()` and `Element::toXml()` path. +- `DeviceInformation` is a typed view over that existing context. It does not + introduce a second data store. Updates made by the properties editor continue + to use `ChangeElementInformationCommand`, including undo/redo and dirty state. +- The project database copies element contexts into `element_info` and exposes + reporting fields through `element_nomenclature_view`. +- GUI and CLI BOM exports both use `BomExport::defaultQuery()` against that + view. The existing `BOMExportDialog` remains the only GUI export entry point; + users can still customize columns, filters, and grouped/ungrouped output. ## Device Properties -`sources/properties/deviceinformation.h/.cpp` provide a small value type -and mapping to the existing context. There is no parallel device store. -`toContext(existing_context)` preserves unrelated properties and visibility -flags. Callers changing a placed element should use -`ChangeElementInformationCommand` to retain undo/redo and dirty tracking. +| Device field | XML/context key | Default BOM | +| --- | --- | --- | +| Device tag | `label` | Yes | +| Manufacturer | `manufacturer` | Yes | +| Part number | `manufacturer_reference` | Yes | +| Model | `model` | Yes | +| Description | `description` | Yes | +| Category | `category` | Yes | +| Quantity | `quantity` | Yes | +| Voltage rating | `voltage_rating` | Yes | +| Current rating | `current_rating` | Yes | +| Page | `folio` from the diagram | Yes | +| Notes | `notes` | Yes | -| Device field | XML/context key or source | Origin | CSV BOM | -| --- | --- | --- | --- | -| Device tag | `label` | Existing; BOM resolves formula labels | Yes | -| Manufacturer | `manufacturer` | Existing | Yes | -| Part number | `manufacturer_reference` | Existing | Yes | -| Model | `model` | New | Yes | -| Description | `description` | Existing | Yes | -| Category | `category` | New | Yes | -| Quantity | `quantity` | Existing | Yes | -| Page | Resolved diagram folio | Derived | Yes | -| Voltage rating | `voltage_rating` | New | No; stored metadata | -| Current rating | `current_rating` | New | No; stored metadata | -| Notes | `notes` | New | Yes | +The five new context keys are `model`, `category`, `voltage_rating`, +`current_rating`, and `notes`. They are available in the existing information +editor for Simple and Master elements, in symbol definitions, in the SQL +nomenclature view, and in the export column picker. -No XML version bump or migration is needed. Absent properties remain empty; -empty properties are omitted by the existing XML writer. Older keys keep -their original meaning. Generic context loading preserves unknown keys. -As before, serialization trims leading/trailing whitespace. Compatibility -with every historical QET release is not implied. - -Quantity remains text, as in existing QET properties, so decimal quantities -and existing unit conventions are not silently changed. A missing quantity -stays empty in the BOM: it is not silently assumed to be one. Ratings are -text (for example `24 V DC` and `16 A`). No numeric aggregation is performed. - -The five new rows are automatically available in the existing information -tab for normal/master symbols and in the symbol definition editor. No new -panel is required. Notes use the existing single-line property editor; -programmatically loaded multiline values are supported by XML and CSV. -New keys are also exposed in the existing SQL nomenclature view. +No XML version bump or migration is required. Missing keys read as empty +strings, empty properties are omitted by the existing writer, and unknown +context keys remain preserved. Quantity and ratings stay free text so existing +unit conventions and decimal quantities are not changed implicitly. ## BOM Policy -`sources/devicebomexport.h/.cpp` collect current in-memory project data, -including unsaved property changes, without a SQL cache dependency. +The default BOM query exports one row per Simple or Master element. Slave +contacts, terminal-strip elements, reports, thumbnails, and conductor +definitions are excluded. A Master device is counted once and PLC I/O entries +are not expanded. The existing `exclude_from_bom` property is honored by the +SQL view, including common true representations (`true`, `1`, `yes`, `on`). -- One row per Simple or Master element. -- Slave symbols, terminals, page reports, thumbnails and conductor definitions - are excluded; a master is counted once, not once per contact representation. - Even unlinked slave symbols are excluded, matching this device-level policy. -- The existing `exclude_from_bom` property is respected. -- Empty tags and part numbers do not discard otherwise eligible elements. -- Rows follow project page order, then device tag, then UUID for stable ties. - `Page` is the resolved folio label shown by QET; an empty label falls back - to the one-based project page position. -- Identical part numbers remain separate rows. Grouping is deferred because - conflicting manufacturers, ratings, tags and quantities need explicit rules. -- Auxiliary accessories are not expanded into extra rows in this MVP. -- A PLC master can be listed as one physical Master device, but its embedded - PLC I/O entries are never expanded into BOM rows. +The default columns are: -The Project menu action **Export BOM as CSV** exports fixed columns: -`No, Device tag, Manufacturer, Part number, Model, Description, Category, -Quantity, Page, Notes`. The legacy configurable export and CLI export retain -their previous behavior. Voltage and current ratings deliberately remain -device metadata in this MVP because they were not part of the requested BOM -schema; they can be added later without changing the stored project data. +`Device tag, Designation, Manufacturer, Part number, Model, Description, +Category, Quantity, Voltage rating, Current rating, Page, Notes`. -CSV is UTF-8 with a BOM, comma-delimited, CRLF record endings, double-quoted -fields and doubled embedded quotes. Embedded newlines are retained. Values -are exported verbatim so device tags and part numbers are not altered. CSV -quoting does not disable spreadsheet formulas: only export trusted projects, -or import all columns as **text** when inspecting untrusted project data. Text -import also preserves leading zeros. -`QSaveFile` commits atomically; open, write and commit errors are reported. -Cancel does not write a file. Read-only projects can still be exported. +The existing **Format as bill of materials** checkbox provides grouped output; +it is off by default so the initial export remains ungrouped. Users may change +the query and column order through the existing dialog. The command-line +`--export-bom` operation uses the same default query and column order. + +CSV output is UTF-8 with a BOM, semicolon-delimited, and quotes every field. +Embedded quotes are doubled and embedded newlines are retained. `QSaveFile` +provides atomic replacement; query, open, write, and commit failures are +reported without deleting an existing destination first. ## Verification -On the configured Windows/MSYS2 Qt5 system, from the repository root: +The smart-device test is registered explicitly in +`tests/qttest/CMakeLists.txt` and uses only Qt Test and Qt SQL. It is therefore +part of the normal cross-platform CMake/CTest flow: -```powershell -.\tests\qttest\run-smart-device-tests.ps1 +```sh +cmake -S . -B build -DPACKAGE_TESTS=ON +cmake --build build +ctest --test-dir build --output-on-failure ``` -The runner builds the application with `debug` and `no_kf5`, then links a QtTest -executable against the real application objects. Use `-SkipApplicationBuild` -only after building the current application sources. Results and generated -sample `.qet`, CSV and properties screenshot are under -`build-debug/smart-device-tests/`. CMake's explicit source list includes the -new files; qmake discovers them through its existing globs. - -Tests cover absent/legacy properties, context/XML round trips, custom-key -preservation, project save/reopen, BOM filtering and ordering, duplicate part -numbers, CSV encoding/escaping, write success/failure, properties UI undo/redo, -and the existing database report view. +`tst_smart_device` checks the metadata column contract, Simple/Master filtering, +stable page ordering, UTF-8 with Latin and non-Latin samples, CSV escaping, and +atomic write success/failure. Existing project XML save/load tests cover the +generic `DiagramContext` persistence path used by the new keys. Manual checks: -1. Open an existing project and edit a normal or master element's information - tab. Existing fields and the new Model/Category/ratings/Notes rows must show. -2. Enter manufacturer, part number, quantity and new properties. Apply, undo, - redo, save, close and reopen. Verify the same values are present. -3. Give two elements the same part number on different pages. Export through - Project > Export BOM as CSV. Expect separate rows with their own tags/pages. -4. Enable the existing exclusion checkbox for one element and export again. - Its row must be absent; changing a property without saving must affect CSV. -5. Open CSV with a UTF-8 CSV reader (import text columns when using Excel). - Check Turkish characters, leading-zero part numbers, commas and quotes. -6. Cancel export and try a non-writable destination. Cancellation must not - create a file; failure must show an error without destroying an existing CSV. +1. Open an existing project and edit a Simple or Master element's information. +2. Set all device fields, apply, undo, redo, save, close, and reopen. +3. Open Project > Export as CSV and verify the device columns are preselected. +4. Export with grouping both disabled and enabled. +5. Verify leading-zero part numbers, non-Latin text, quotes, and newlines in a + UTF-8 CSV reader. -Suggested review pieces: (1) device mapping and property/report registration, -(2) collector and CSV menu action, (3) tests and documentation. No cable, -PLC I/O, catalog integration or cabinet-layout feature is added. +Cable management, terminal-strip planning, automatic wire numbering, PLC I/O +expansion, panel layout, and manufacturer catalog integration are out of scope. diff --git a/lang/qet_en.ts b/lang/qet_en.ts index c8776c3f0..bcf971599 100644 --- a/lang/qet_en.ts +++ b/lang/qet_en.ts @@ -7568,34 +7568,6 @@ Available options: Exporter au format CSV Export to CSV - - - - Exporter la nomenclature des appareils au format CSV - Export device BOM as CSV - - - - Fichiers CSV (*.csv) - CSV files (*.csv) - - - - Exporter la nomenclature - Export BOM - - - - Impossible d'enregistrer la nomenclature dans %1. -%2 - Could not save the BOM to %1. -%2 - - - - Nomenclature exportée dans %1 - BOM exported to %1 - Gestionnaire de borniers (DEV) @@ -10995,31 +10967,6 @@ Do you want to replace it ? Description textuelle Textual description - - - Modèle - Model - - - - Catégorie - Category - - - - Tension nominale - Voltage rating - - - - Courant nominal - Current rating - - - - Notes - Notes - Numéro d'article diff --git a/lang/qet_tr.ts b/lang/qet_tr.ts index 69f34dc90..ec58ef57e 100644 --- a/lang/qet_tr.ts +++ b/lang/qet_tr.ts @@ -7685,34 +7685,6 @@ Mevcut seçenekler: Exporter au format CSV CSV formatında export et - - - - Exporter la nomenclature des appareils au format CSV - Cihaz malzeme listesini CSV olarak dışa aktar - - - - Fichiers CSV (*.csv) - CSV dosyaları (*.csv) - - - - Exporter la nomenclature - Malzeme listesini dışa aktar - - - - Impossible d'enregistrer la nomenclature dans %1. -%2 - Malzeme listesi %1 konumuna kaydedilemedi. -%2 - - - - Nomenclature exportée dans %1 - Malzeme listesi %1 konumuna aktarıldı - Gestionnaire de borniers (DEV) @@ -10995,31 +10967,6 @@ Değiştirmek ister misiniz? Description textuelle Metin açıklaması - - - Modèle - Model - - - - Catégorie - Kategori - - - - Tension nominale - Anma gerilimi - - - - Courant nominal - Anma akımı - - - - Notes - Notlar - Numéro d'article diff --git a/sources/bomexport.cpp b/sources/bomexport.cpp new file mode 100644 index 000000000..eefdd2e94 --- /dev/null +++ b/sources/bomexport.cpp @@ -0,0 +1,113 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "bomexport.h" + +#include +#include +#include + +namespace { +QString csvField(QString value) +{ + value.replace(QLatin1Char('"'), QStringLiteral("\"\"")); + return QLatin1Char('"') + value + QLatin1Char('"'); +} + +QByteArray csvRecord(const QStringList &values) +{ + QStringList escaped; + for (const auto &value : values) { + escaped.append(csvField(value)); + } + return (escaped.join(QLatin1Char(';')) + QLatin1Char('\n')).toUtf8(); +} +} + +QStringList BomExport::defaultColumns() +{ + return { + QStringLiteral("label"), + QStringLiteral("designation"), + QStringLiteral("manufacturer"), + QStringLiteral("manufacturer_reference"), + QStringLiteral("model"), + QStringLiteral("description"), + QStringLiteral("category"), + QStringLiteral("quantity"), + QStringLiteral("voltage_rating"), + QStringLiteral("current_rating"), + QStringLiteral("folio"), + QStringLiteral("notes") + }; +} + +QString BomExport::defaultQuery() +{ + return QStringLiteral("SELECT %1 FROM element_nomenclature_view " + "WHERE ( element_type = 'simple' OR element_type = 'master') " + "ORDER BY diagram_position, position, label") + .arg(defaultColumns().join(QStringLiteral(", "))); +} + +QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers, + bool include_headers, int *row_count) +{ + QByteArray csv("\xEF\xBB\xBF"); + if (include_headers) { + csv += csvRecord(headers); + } + int rows = 0; + while (query.next()) + { + QStringList values; + for (int i = 0; i < query.record().count(); ++i) { + values.append(query.value(i).toString()); + } + csv += csvRecord(values); + ++rows; + } + if (row_count) { + *row_count = rows; + } + return csv; +} + +bool BomExport::writeCsv(const QString &file_path, const QByteArray &csv, + QString *error) +{ + if (error) { + error->clear(); + } + QSaveFile file(file_path); + if (!file.open(QIODevice::WriteOnly) || file.write(csv) != csv.size()) + { + if (error) { + *error = file.errorString(); + } + file.cancelWriting(); + return false; + } + if (!file.commit()) + { + if (error) { + *error = file.errorString(); + } + return false; + } + return true; +} diff --git a/sources/devicebomexport.h b/sources/bomexport.h similarity index 61% rename from sources/devicebomexport.h rename to sources/bomexport.h index 1fe0332d0..0241165a0 100644 --- a/sources/devicebomexport.h +++ b/sources/bomexport.h @@ -15,29 +15,23 @@ You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ -#ifndef DEVICEBOMEXPORT_H -#define DEVICEBOMEXPORT_H - -#include "properties/deviceinformation.h" +#ifndef BOMEXPORT_H +#define BOMEXPORT_H #include -#include #include +#include -class QETProject; +class QSqlQuery; -class DeviceBomExport +namespace BomExport { - public: - struct Row { - DeviceInformation m_device; - QString m_page; - }; + QStringList defaultColumns(); + QString defaultQuery(); + QByteArray toCsv(QSqlQuery &query, const QStringList &headers, + bool include_headers = true, int *row_count = nullptr); + bool writeCsv(const QString &file_path, const QByteArray &csv, + QString *error = nullptr); +} - static QList collect(const QETProject &project); - static QByteArray toCsv(const QList &rows); - static bool writeCsv(const QETProject &project, const QString &file_path, - QString *error = nullptr); -}; - -#endif // DEVICEBOMEXPORT_H +#endif // BOMEXPORT_H diff --git a/sources/cli_export.cpp b/sources/cli_export.cpp index 16b9b15c7..a132a8e29 100644 --- a/sources/cli_export.cpp +++ b/sources/cli_export.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "cli_export.h" +#include "bomexport.h" #include "bordertitleblock.h" #include "conductornumexport.h" @@ -304,61 +305,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output) return 0; } -/// Quote a field for CSV output (RFC-4180 style, ';' delimiter). -QString csvField(const QString &value) -{ - if (value.contains(';') || value.contains('"') - || value.contains('\n') || value.contains('\r')) { - QString v = value; - v.replace('"', "\"\""); - return '"' % v % '"'; - } - return value; -} - -/// Bill of materials: one row per element, key component-data fields. -/// Pulls from QET's own project database (the same source as the GUI BOM -/// export), so the output matches what the editor produces. +/// Bill of materials from the same project database and default query as the +/// GUI nomenclature export. int exportBom(QETProject &project, const QString &output) { - // The project database is built lazily; force a (re)build before querying. project.dataBase()->updateDB(); - - static const QStringList columns { - "label", "designation", "manufacturer", "manufacturer_reference", - "quantity", "location", "function", "title", "folio" - }; - - QSqlQuery query = project.dataBase()->newQuery( - "SELECT " % columns.join(", ") % - " FROM element_nomenclature_view ORDER BY label"); + QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery()); if (!query.exec()) { err << "BOM query failed: " << query.lastError().text() << "\n"; return 1; } - - QString csv = columns.join(";") % "\n"; int rows = 0; - while (query.next()) { - QStringList values; - for (int i = 0; i < columns.size(); ++i) - values << csvField(query.value(i).toString()); - csv += values.join(";") % "\n"; - ++rows; - } - - QFile file(output); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - err << "Cannot open '" << output << "' for writing.\n"; + const auto csv = BomExport::toCsv( + query, BomExport::defaultColumns(), true, &rows); + QString error; + if (!BomExport::writeCsv(output, csv, &error)) { + err << "Cannot write '" << output << "': " << error << "\n"; return 1; } - QTextStream fout(&file); - fout << csv; - file.close(); out << "Exported " << rows << " component(s) -> " << output << "\n"; return 0; } +QString csvField(const QString &value) +{ + if (value.contains(QLatin1Char(';')) || value.contains(QLatin1Char('"')) + || value.contains(QLatin1Char('\n')) || value.contains(QLatin1Char('\r'))) + { + QString escaped = value; + escaped.replace(QLatin1Char('"'), QStringLiteral("\"\"")); + return QLatin1Char('"') % escaped % QLatin1Char('"'); + } + return value; +} + /// Count terminals on @p element that no conductor connects to. int freeTerminals(Element *element) { diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index bc3711737..1549f6ce0 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -663,7 +663,8 @@ void projectDataBase::createElementNomenclatureView() "di.folio AS folio," "e.pos AS position " " FROM element_info ei, diagram_info di, element e, diagram d" - " WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid AND (ei.exclude_from_bom IS NOT 'true')" + " WHERE ei.element_uuid = e.uuid AND e.diagram_uuid = d.uuid AND di.diagram_uuid = d.uuid" + " AND COALESCE(LOWER(TRIM(ei.exclude_from_bom)), '') NOT IN ('true', '1', 'yes', 'on')" //The element table holds every element of the project; which //kinds belong in a nomenclature is this view's business, not //the table's. Kept identical to the mask populateElementTable() diff --git a/sources/dataBase/ui/elementquerywidget.cpp b/sources/dataBase/ui/elementquerywidget.cpp index fff775b82..aafdeb32d 100644 --- a/sources/dataBase/ui/elementquerywidget.cpp +++ b/sources/dataBase/ui/elementquerywidget.cpp @@ -160,6 +160,8 @@ void ElementQueryWidget::setQuery(const QString &query) { auto str_type = rxm.captured(1); where.remove(str_type); + const bool all_master_types = str_type.contains( + ElementData::typeToString(ElementData::Master)); int c=0; ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false); @@ -170,23 +172,23 @@ void ElementQueryWidget::setQuery(const QString &query) if (ui->m_terminal_cb->isChecked()) { ++c; } - ui->m_coil_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Coil)) ? true : false); + ui->m_coil_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Coil))); if (ui->m_coil_cb->isChecked()) { ++c; } - ui->m_button_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::Commutator)) ? true : false); + ui->m_button_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Commutator))); if (ui->m_button_cb->isChecked()) { ++c; } - ui->m_protection_cb->setChecked(str_type.contains(ElementData::masterTypeToString(ElementData::Protection)) ? true : false); - if (ui->m_protection_cb) { + ui->m_protection_cb->setChecked(all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Protection))); + if (ui->m_protection_cb->isChecked()) { ++c; } ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false); if (ui->m_thumbnail_cb->isChecked()) { ++c; } - ui->m_plc_cb->setChecked (str_type.contains(ElementData::masterTypeToString(ElementData::PLC)) ? true : false); + ui->m_plc_cb->setChecked (all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::PLC))); if (ui->m_plc_cb->isChecked()) { ++c; } @@ -352,22 +354,32 @@ QString ElementQueryWidget::queryStr() const where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'"; b = true; } - if (ui->m_button_cb->isChecked()) { + const bool all_master_types = ui->m_button_cb->isChecked() + && ui->m_coil_cb->isChecked() + && ui->m_protection_cb->isChecked() + && ui->m_plc_cb->isChecked(); + if (all_master_types) { + if (b) where += " OR"; + where += QStringLiteral(" element_type = '") + += ElementData::typeToString(ElementData::Master) += "'"; + b = true; + } + if (!all_master_types && ui->m_button_cb->isChecked()) { if (b) where +=" OR"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'"; b = true; } - if (ui->m_coil_cb->isChecked()) { + if (!all_master_types && ui->m_coil_cb->isChecked()) { if (b) where +=" OR"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'"; b = true; } - if (ui->m_protection_cb->isChecked()) { + if (!all_master_types && ui->m_protection_cb->isChecked()) { if (b) where +=" OR"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'"; b = true; } - if (ui->m_plc_cb->isChecked()) { + if (!all_master_types && ui->m_plc_cb->isChecked()) { if (b) where +=" OR"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'"; b = true; diff --git a/sources/devicebomexport.cpp b/sources/devicebomexport.cpp deleted file mode 100644 index 485d341c5..000000000 --- a/sources/devicebomexport.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/* - Copyright 2006-2026 The QElectroTech Team - This file is part of QElectroTech. - - QElectroTech is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - QElectroTech 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with QElectroTech. If not, see . -*/ -#include "devicebomexport.h" - -#include "diagram.h" -#include "qetgraphicsitem/element.h" -#include "qetproject.h" - -#include -#include -#include - -namespace { -QByteArray csvRecord(const QStringList &values) -{ - QStringList escaped; - for (QString value : values) { - value.replace(QLatin1Char('"'), QStringLiteral("\"\"")); - escaped.append(QLatin1Char('"') + value + QLatin1Char('"')); - } - return (escaped.join(QLatin1Char(',')) + QStringLiteral("\r\n")).toUtf8(); -} -} - -QList DeviceBomExport::collect(const QETProject &project) -{ - QList rows; - int page = 0; - for (Diagram *diagram : project.diagrams()) - { - const QString page_position = QString::number(++page); - const QString page_label = diagram->border_and_titleblock.finalfolio().isEmpty() - ? page_position - : diagram->border_and_titleblock.finalfolio(); - auto elements = diagram->elements(); - // Scene stacking order is not stable across save/load. UUID breaks tag ties. - std::sort(elements.begin(), elements.end(), [](Element *a, Element *b) { - const int compare = QString::compare(a->actualLabel(), b->actualLabel()); - return compare ? compare < 0 : a->uuid().toString() < b->uuid().toString(); - }); - for (Element *element : elements) - { - const auto type = element->elementData().m_type; - // Slaves are representations of a master, not additional devices. - // Terminal-specific inventory remains outside this MVP. - if (type != ElementData::Simple && type != ElementData::Master) { - continue; - } - const auto context = element->elementInformations(); - if (context.value(QStringLiteral("exclude_from_bom")).toBool()) { - continue; - } - auto device = DeviceInformation::fromContext(context); - device.m_device_tag = element->actualLabel(); - rows.append({device, page_label}); - } - } - return rows; -} - -QByteArray DeviceBomExport::toCsv(const QList &rows) -{ - // UTF-8 BOM lets desktop spreadsheet applications detect Turkish text too. - QByteArray csv("\xEF\xBB\xBF"); - csv += csvRecord({QStringLiteral("No"), QStringLiteral("Device tag"), - QStringLiteral("Manufacturer"), QStringLiteral("Part number"), - QStringLiteral("Model"), QStringLiteral("Description"), - QStringLiteral("Category"), QStringLiteral("Quantity"), - QStringLiteral("Page"), QStringLiteral("Notes")}); - int number = 0; - for (const auto &row : rows) - { - const auto &device = row.m_device; - csv += csvRecord({QString::number(++number), device.m_device_tag, - device.m_manufacturer, device.m_part_number, device.m_model, - device.m_description, device.m_category, device.m_quantity, - row.m_page, device.m_notes}); - } - return csv; -} - -bool DeviceBomExport::writeCsv(const QETProject &project, const QString &file_path, - QString *error) -{ - if (error) { - error->clear(); - } - QSaveFile file(file_path); - if (!file.open(QIODevice::WriteOnly)) { - if (error) *error = file.errorString(); - return false; - } - const auto csv = toCsv(collect(project)); - if (file.write(csv) != csv.size()) { - if (error) *error = file.errorString(); - file.cancelWriting(); - return false; - } - if (!file.commit()) { - if (error) *error = file.errorString(); - return false; - } - return true; -} diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index d04f102d3..724fde835 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -21,7 +21,6 @@ #include "QWidgetAnimation/qwidgetanimation.h" #include "autoNum/ui/autonumberingdockwidget.h" #include "conductornumexport.h" -#include "devicebomexport.h" #include "diagramcommands.h" #include "diagramevent/diagrameventaddimage.h" #ifdef QET_HAS_QTPDF @@ -471,34 +470,6 @@ void QETDiagramEditor::setUpActions() bom.exec(); }); - m_device_bom_export = new QAction(QET::Icons::DocumentSpreadsheet, - tr("Exporter la nomenclature des appareils au format CSV"), this); - m_device_bom_export->setObjectName(QStringLiteral("export_device_bom_csv")); - connect(m_device_bom_export, &QAction::triggered, this, [this]() { - auto *view = currentProjectView(); - if (!view) return; - QPointer project = view->project(); - QString dir = project->currentDir(); - if (dir.isEmpty()) dir = QETApp::documentDir(); - QFileDialog dialog( - this, - tr("Exporter la nomenclature des appareils au format CSV"), - QDir(dir).filePath(QStringLiteral("bom.csv")), - tr("Fichiers CSV (*.csv)")); - dialog.setAcceptMode(QFileDialog::AcceptSave); - dialog.setDefaultSuffix(QStringLiteral("csv")); - if (dialog.exec() != QDialog::Accepted || !project) return; - const QString path = dialog.selectedFiles().value(0); - if (path.isEmpty()) return; - QString error; - if (!DeviceBomExport::writeCsv(*project, path, &error)) { - QMessageBox::critical(this, tr("Exporter la nomenclature"), - tr("Impossible d'enregistrer la nomenclature dans %1.\n%2").arg(path, error)); - } else { - statusBar()->showMessage(tr("Nomenclature exportée dans %1").arg(path), 5000); - } - }); - //Add a nomenclature item m_add_nomenclature = new QAction(QET::Icons::TableOfContent, tr("Ajouter une nomenclature"), this); connect(m_add_nomenclature, &QAction::triggered, this, [=]() { @@ -977,7 +948,6 @@ void QETDiagramEditor::setUpMenu() menu_project -> addAction(m_add_summary); menu_project -> addAction(m_add_nomenclature); menu_project -> addAction(m_csv_export); - menu_project -> addAction(m_device_bom_export); menu_project -> addAction(m_project_export_conductor_num); menu_project -> addAction(m_terminal_strip_dialog); menu_project -> addAction(m_project_terminalBloc); @@ -1829,7 +1799,6 @@ void QETDiagramEditor::slot_updateActions() m_add_summary -> setEnabled(editable_project); m_add_nomenclature -> setEnabled(editable_project); m_csv_export -> setEnabled(editable_project); - m_device_bom_export -> setEnabled(opened_project); m_project_export_conductor_num-> setEnabled(opened_project); m_terminal_strip_dialog -> setEnabled(editable_project); m_project_export_wiring_list -> setEnabled(opened_project); diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index 313d5e803..ed07a2395 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -203,7 +203,6 @@ class QETDiagramEditor : public QETMainWindow *m_remove_diagram_from_project, ///< Delete a diagram from the current project *m_clean_project, ///< Clean the content of the current project by removing useless items *m_csv_export, ///< generate nomenclature - *m_device_bom_export, ///< Export an ungrouped device BOM *m_add_nomenclature, ///< Add nomenclature graphics item; *m_add_summary, ///m_main_layout->insertWidget(0, m_query_widget); - //By default format as bom is clicked - on_m_format_as_bom_clicked(true); + m_query_widget->setQuery(BomExport::defaultQuery()); + on_m_format_as_bom_clicked(false); } /** @@ -67,90 +68,58 @@ int BOMExportDialog::exec() if (dir.isEmpty()) dir = QETApp::documentDir(); QString file_name = dir % "/" % tr("nomenclature_") % QString(m_project ->title() % ".csv"); QString file_path = QFileDialog::getSaveFileName(this, tr("Enregister sous... "), file_name, tr("Fichiers csv (*.csv)")); - QFile file(file_path); if (!file_path.isEmpty()) { - if (QFile::exists(file_path )) - { - // if file already exist -> delete it - if (!QFile::remove(file_path) ) - { - QMessageBox::critical(this, tr("Erreur"), - tr("Impossible de remplacer le fichier!\n\n")+ - "Destination : "+file_path+"\n"); - } - } - if (file.open(QIODevice::WriteOnly | QIODevice::Text)) - { - QTextStream stream(&file); - stream << getBom() << &Qt::endl(stream); + QString error; + const auto csv = getBom(&error); + if (!error.isEmpty() || !BomExport::writeCsv(file_path, csv, &error)) { + QMessageBox::critical( + this, tr("Erreur"), + tr("Impossible d'enregistrer la nomenclature dans %1.\n%2") + .arg(file_path, error)); } } } return r; } -QString BOMExportDialog::getBom() +QByteArray BOMExportDialog::getBom(QString *error) { + if (error) { + error->clear(); + } m_project->dataBase()->updateDB(); auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr()); - QString return_string; if (!query_.exec()) { qDebug() << "BOMExportDialog::getBom : query errir : " << query_.lastError(); + if (error) { + *error = query_.lastError().text(); + } + return {}; } - else + + QStringList header_names; + if (ui->m_include_headers->isChecked()) { - //HEADERS - if (ui->m_include_headers) + const auto record_ = query_.record(); + for (int i = 0; i < record_.count(); ++i) { - auto record_ = query_.record(); - QStringList header_name; - for (auto i=0 ; im_include_headers->isChecked()); } /** diff --git a/sources/ui/bomexportdialog.h b/sources/ui/bomexportdialog.h index 5140e26b3..36fa40d15 100644 --- a/sources/ui/bomexportdialog.h +++ b/sources/ui/bomexportdialog.h @@ -19,6 +19,7 @@ #define BOMEXPORTDIALOG_H #include +#include class QETProject; class ElementQueryWidget; @@ -39,7 +40,7 @@ class BOMExportDialog : public QDialog ~BOMExportDialog() override; virtual int exec() override; - QString getBom(); + QByteArray getBom(QString *error = nullptr); private slots: void on_m_format_as_bom_clicked(bool checked); diff --git a/sources/ui/bomexportdialog.ui b/sources/ui/bomexportdialog.ui index e578d0399..b9e60b329 100644 --- a/sources/ui/bomexportdialog.ui +++ b/sources/ui/bomexportdialog.ui @@ -36,7 +36,7 @@ Formater en tant que liste de materiel - true + false diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 94d082282..4fec2b9a3 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -91,3 +91,12 @@ add_executable(tst_contactusage tst_contactusage.cpp) add_test(NAME tst_contactusage COMMAND tst_contactusage) target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources) target_link_libraries(tst_contactusage PRIVATE Qt::Test) + +add_executable( + tst_smart_device + tst_smart_device.cpp + ${QET_DIR}/sources/bomexport.cpp + ${QET_DIR}/sources/bomexport.h) +add_test(NAME tst_smart_device COMMAND tst_smart_device) +target_include_directories(tst_smart_device PRIVATE ${QET_DIR}/sources) +target_link_libraries(tst_smart_device PRIVATE Qt::Test Qt::Sql) diff --git a/tests/qttest/run-smart-device-tests.ps1 b/tests/qttest/run-smart-device-tests.ps1 deleted file mode 100644 index ef90156f0..000000000 --- a/tests/qttest/run-smart-device-tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -param( - [string]$MsysRoot = 'C:\msys64', - [string]$BuildDir = (Join-Path $PSScriptRoot '..\..\build-debug'), - [int]$Jobs = 8, - [switch]$SkipApplicationBuild -) - -$ErrorActionPreference = 'Stop' -$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path -$BuildDir = [IO.Path]::GetFullPath($BuildDir) -$oldPath = $env:Path -$oldPlatform = $env:QT_QPA_PLATFORM -$oldFontDir = $env:QT_QPA_FONTDIR -$oldArtifacts = $env:QET_SMART_DEVICE_ARTIFACT_DIR -try { - $env:Path = "$MsysRoot\ucrt64\bin;$MsysRoot\usr\bin;$env:Path" - New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null - Push-Location $BuildDir - try { - if (-not $SkipApplicationBuild) { - & qmake-qt5 (Join-Path $repo 'qelectrotech.pro') -spec win32-g++ CONFIG+=debug CONFIG+=no_kf5 CONFIG-=release DEFINES+=BUILD_WITHOUT_KF - if ($LASTEXITCODE) { throw 'Application qmake failed' } - & mingw32-make "-j$Jobs" - if ($LASTEXITCODE) { throw 'Application build failed' } - } - } finally { Pop-Location } - - $testDir = Join-Path $BuildDir 'smart-device-tests' - New-Item -ItemType Directory -Force -Path $testDir | Out-Null - Push-Location $testDir - try { - & qmake-qt5 (Join-Path $PSScriptRoot 'smart_device.pro') "QET_BUILD_DIR=$($BuildDir.Replace('\', '/'))" CONFIG+=debug CONFIG-=release - if ($LASTEXITCODE) { throw 'Test qmake failed' } - & mingw32-make "-j$Jobs" - if ($LASTEXITCODE) { throw 'Test build failed' } - $env:QT_QPA_PLATFORM = 'offscreen' - $env:QT_QPA_FONTDIR = Join-Path $env:WINDIR 'Fonts' - $env:QET_SMART_DEVICE_ARTIFACT_DIR = Join-Path $testDir 'artifacts' - & .\debug\tst_smart_device.exe -o results.txt,txt - $result = $LASTEXITCODE - Get-Content results.txt - if ($result) { throw "Smart device tests failed: $result" } - } finally { Pop-Location } -} finally { - $env:Path = $oldPath - $env:QT_QPA_PLATFORM = $oldPlatform - $env:QT_QPA_FONTDIR = $oldFontDir - $env:QET_SMART_DEVICE_ARTIFACT_DIR = $oldArtifacts -} diff --git a/tests/qttest/smart_device.pro b/tests/qttest/smart_device.pro deleted file mode 100644 index 124a3abd4..000000000 --- a/tests/qttest/smart_device.pro +++ /dev/null @@ -1,24 +0,0 @@ -QT += widgets xml sql network concurrent svg printsupport testlib -CONFIG += console testcase c++17 -CONFIG -= app_bundle -TEMPLATE = app -TARGET = tst_smart_device - -# Link the real application objects, built with the same Qt and no_kf5 config. -isEmpty(QET_BUILD_DIR): QET_BUILD_DIR = $$clean_path($$PWD/../../build-debug) -INCLUDEPATH += $$PWD/../.. $$PWD/../../sources $$PWD/../../SingleApplication -INCLUDEPATH += $$PWD/../../sources/ui $$PWD/../../sources/TerminalStrip/ui -INCLUDEPATH += $$PWD/../../sources/ui/nokde $$QET_BUILD_DIR -DEFINES += BUILD_WITHOUT_KF BUILD_WITHOUT_KF5 QAPPLICATION_CLASS=QApplication -SOURCES += $$PWD/tst_smart_device.cpp -win32 { - QET_OBJECTS = $$files($$QET_BUILD_DIR/debug/*.o) - QET_OBJECTS -= $$QET_BUILD_DIR/debug/main.o - LIBS += -lsqlite3 -ladvapi32 -} else { - QET_OBJECTS = $$files($$QET_BUILD_DIR/*.o) - QET_OBJECTS -= $$QET_BUILD_DIR/main.o - LIBS += -lsqlite3 -} -isEmpty(QET_OBJECTS): error(Build the application before running these tests) -OBJECTS += $$QET_OBJECTS diff --git a/tests/qttest/tst_smart_device.cpp b/tests/qttest/tst_smart_device.cpp index 6ea603783..b054a6eba 100644 --- a/tests/qttest/tst_smart_device.cpp +++ b/tests/qttest/tst_smart_device.cpp @@ -15,311 +15,121 @@ You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ -#include -#include -#include -#include -#include +#include "bomexport.h" -#include "ElementsCollection/xmlelementcollection.h" -#include "dataBase/projectdatabase.h" -#include "devicebomexport.h" -#include "diagram.h" -#include "qetgraphicsitem/element.h" -#include "qetinformation.h" -#include "qetproject.h" -#include "qetresult.h" -#include "ui/elementinfopartwidget.h" -#include "ui/elementinfowidget.h" +#include +#include +#include +#include +#include +#include class SmartDeviceTest : public QObject { Q_OBJECT - QTemporaryDir m_settings; - Element *addElement(QETProject &project, Diagram *diagram, const QString &tag, - ElementData::Type type = ElementData::Simple) - { - QDomDocument doc; - doc.setContent(QStringLiteral( - "" - "Test device" - "")); - const QString name = QUuid::createUuid().toString(QUuid::WithoutBraces) + ".elmt"; - if (!project.embeddedElementCollection()->addElementDefinition("import", name, doc.documentElement())) - return nullptr; - int state = -1; - auto *element = new Element(ElementsLocation("embed://import/" + name, &project), nullptr, &state); - if (state != 0) { - delete element; - return nullptr; - } - diagram->addItem(element); - auto data = element->elementData(); - data.m_type = type; - data.m_informations.addValue("label", tag); - element->setElementData(data); - return element; - } - - DeviceInformation sample() const - { - DeviceInformation info; - info.m_device_tag = "-K1"; - info.m_manufacturer = QString::fromUtf8("\xC3\x96rnek Elektrik"); - info.m_part_number = "001234"; - info.m_model = "RX-24"; - info.m_description = QString::fromUtf8("G\xC3\xBC\xC3\xA7 kontakt\xC3\xB6r\xC3\xBC"); - info.m_category = "Contactor"; - info.m_quantity = "2.5"; - info.m_voltage_rating = "24 V DC"; - info.m_current_rating = "16 A"; - info.m_notes = "Panel A, \"spare\"; check\nSecond line"; - return info; - } - -private slots: - void initTestCase() - { - QCoreApplication::setOrganizationName("QET-SmartDeviceTests"); - QCoreApplication::setApplicationName("IsolatedTests"); - QSettings::setDefaultFormat(QSettings::IniFormat); - QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, m_settings.path()); - QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, m_settings.path()); - QETProject::setBackupEnabled(false); - } - - void emptyAndLegacyContext() - { - const auto empty = DeviceInformation::fromContext(DiagramContext()); - QVERIFY(empty.toContext().keys().isEmpty()); - QVERIFY(empty.m_quantity.isEmpty()); - QDomDocument doc; - QVERIFY(doc.setContent(QStringLiteral( - "K7" - "0007" - ""))); - DiagramContext context; - context.fromXml(doc.documentElement(), "elementInformation"); - const auto legacy = DeviceInformation::fromContext(context); - QCOMPARE(legacy.m_device_tag, QString("K7")); - QCOMPARE(legacy.m_part_number, QString("0007")); - QVERIFY(legacy.m_model.isEmpty()); - QVERIFY(legacy.m_voltage_rating.isEmpty()); - QVERIFY(legacy.m_current_rating.isEmpty()); - QVERIFY(legacy.m_notes.isEmpty()); - } - - void contextRoundTripAndMerge() - { - DiagramContext original; - original.addValue("custom_value", "Keep", false); - original.addValue("manufacturer", "Old", false); - original.addValue("exclude_from_bom", "true"); - auto info = sample(); - auto context = info.toContext(original); - QVERIFY(!context.keyMustShow("manufacturer")); - QVERIFY(!context.keyMustShow("custom_value")); - QCOMPARE(context.value("custom_value").toString(), QString("Keep")); - QDomDocument doc; - auto xml = doc.createElement("elementInformations"); - doc.appendChild(xml); - context.toXml(xml, "elementInformation"); - QDomDocument reparsed; - QVERIFY(reparsed.setContent(doc.toByteArray())); - DiagramContext restored; - restored.fromXml(reparsed.documentElement(), "elementInformation"); - QVERIFY(restored == context); - QVERIFY(DeviceInformation::fromContext(restored).toContext() == info.toContext()); - info.m_model.clear(); - const auto cleared = info.toContext(restored); - QVERIFY(!cleared.contains("model")); - QCOMPARE(cleared.value("exclude_from_bom").toString(), QString("true")); - } - - void projectSaveLoad() - { - QTemporaryDir temp; - QETProject project; - auto *diagram = project.addNewDiagram(); - diagram->border_and_titleblock.setFolio("A-01"); - auto *element = addElement(project, diagram, "-K1"); - QVERIFY(element); - element->setElementInformations(sample().toContext()); - auto *second_diagram = project.addNewDiagram(); - second_diagram->border_and_titleblock.setFolio("A-02"); - diagram->border_and_titleblock.setFolioData(1, 2); - second_diagram->border_and_titleblock.setFolioData(2, 2); - auto *legacy = addElement(project, second_diagram, "-K2"); - QVERIFY(legacy); - const auto before = DeviceBomExport::toCsv(DeviceBomExport::collect(project)); - project.setFilePath(temp.filePath("roundtrip.qet")); - const auto result = project.write(); - QVERIFY2(result.isOk(), qPrintable(result.errorMessage())); - QETProject restored(project.filePath()); - QCOMPARE(restored.state(), QETProject::Ok); - QCOMPARE(restored.diagrams().size(), 2); - const auto rows = DeviceBomExport::collect(restored); - QCOMPARE(rows.size(), 2); - QCOMPARE(rows.first().m_page, QString("A-01")); - QCOMPARE(rows.last().m_page, QString("A-02")); - QVERIFY(rows.first().m_device.toContext() == sample().toContext()); - QVERIFY(rows.last().m_device.m_model.isEmpty()); - QVERIFY(rows.last().m_device.m_quantity.isEmpty()); - QCOMPARE(DeviceBomExport::toCsv(rows), before); - - const QString artifacts = qEnvironmentVariable("QET_SMART_DEVICE_ARTIFACT_DIR"); - if (!artifacts.isEmpty()) { - QDir().mkpath(artifacts); - restored.setFilePath(QDir(artifacts).filePath("sample.qet")); - QVERIFY(restored.write().isOk()); - QVERIFY(DeviceBomExport::writeCsv(restored, QDir(artifacts).filePath("sample.csv"))); - } - } - - void collectDevices() - { - QETProject project; - auto *first = project.addNewDiagram(); - auto *second = project.addNewDiagram(); - first->border_and_titleblock.setFolio("DEV-01"); - second->border_and_titleblock.setFolio("DEV-10"); - first->border_and_titleblock.setFolioData(1, 2); - second->border_and_titleblock.setFolioData(2, 2); - for (auto type : {ElementData::Simple, ElementData::Master, ElementData::Terminal, - ElementData::Slave, ElementData::NextReport, ElementData::PreviousReport, - ElementData::Thumbnail, ElementData::ConductorDefinition}) { - auto *element = addElement(project, first, QString::number(type), type); - QVERIFY(element); - if (type == ElementData::Master) { - auto data = element->elementData(); - data.m_master_type = ElementData::PLC; - data.m_plc_master_data.ios.resize(2); - element->setElementData(data); - } - } - auto *excluded = addElement(project, first, "Excluded"); - QVERIFY(excluded); - auto context = excluded->elementInformations(); - context.addValue("exclude_from_bom", "true"); - excluded->setElementInformations(context); - for (const QString tag : {"Z2", "A2"}) { - auto *element = addElement(project, second, tag); - QVERIFY(element); - auto info = sample(); - info.m_device_tag = tag; - element->setElementInformations(info.toContext()); - } - const auto rows = DeviceBomExport::collect(project); - QCOMPARE(rows.size(), 4); - QCOMPARE(rows.at(0).m_page, QString("DEV-01")); - QCOMPARE(rows.at(0).m_device.m_device_tag, QString("1")); - QCOMPARE(rows.at(1).m_device.m_device_tag, QString("8")); - QCOMPARE(rows.at(2).m_page, QString("DEV-10")); - QCOMPARE(rows.at(2).m_device.m_device_tag, QString("A2")); - QCOMPARE(rows.at(3).m_device.m_device_tag, QString("Z2")); - QCOMPARE(rows.at(2).m_device.m_part_number, rows.at(3).m_device.m_part_number); - QCOMPARE(rows.at(2).m_device.m_quantity, QString("2.5")); - QCOMPARE(DeviceBomExport::toCsv(rows), DeviceBomExport::toCsv(DeviceBomExport::collect(project))); - } - - void emptyFolioFallsBackToPagePosition() - { - QETProject project; - auto *diagram = project.addNewDiagram(); - diagram->border_and_titleblock.setFolio(QString()); - diagram->border_and_titleblock.setFolioData(1, 1); - QVERIFY(addElement(project, diagram, "K1")); - const auto rows = DeviceBomExport::collect(project); - QCOMPARE(rows.size(), 1); - QCOMPARE(rows.first().m_page, QString("1")); - } - - void csvEscapingAndEncoding() - { - const auto csv = DeviceBomExport::toCsv({{sample(), "F-03"}}); - const QByteArray header("\xEF\xBB\xBF\"No\",\"Device tag\",\"Manufacturer\",\"Part number\"," - "\"Model\",\"Description\",\"Category\",\"Quantity\",\"Page\",\"Notes\"\r\n"); - QVERIFY(csv.startsWith(header)); - QVERIFY(csv.contains(sample().m_manufacturer.toUtf8())); - QVERIFY(csv.contains("\"001234\"")); - QVERIFY(!csv.contains("24 V DC")); - QVERIFY(!csv.contains("16 A")); - QVERIFY(csv.endsWith("\"2.5\",\"F-03\",\"Panel A, \"\"spare\"\"; check\nSecond line\"\r\n")); - QCOMPARE(DeviceBomExport::toCsv({}), header); - } - - void fileSuccessAndFailure() - { - QTemporaryDir temp; - QETProject project; - QString error = "old error"; - QVERIFY(!DeviceBomExport::writeCsv(project, temp.filePath("absent/bom.csv"), &error)); - QVERIFY(!error.isEmpty()); - const auto path = temp.filePath("bom.csv"); - QVERIFY(DeviceBomExport::writeCsv(project, path, &error)); - QVERIFY(error.isEmpty()); - QVERIFY(DeviceBomExport::writeCsv(project, path, &error)); - QFile file(path); - QVERIFY(file.open(QIODevice::ReadOnly)); - QCOMPARE(file.readAll(), DeviceBomExport::toCsv({})); - } - - void propertyEditorUndoRedo() - { - QETProject project; - auto *diagram = project.addNewDiagram(); - auto *element = addElement(project, diagram, "-K1"); - QVERIFY(element); - ElementInfoWidget editor(element); - const auto context = sample().toContext(); - const auto widgets = editor.findChildren(); - for (const QString &key : context.keys()) { - auto it = std::find_if(widgets.begin(), widgets.end(), [&key](ElementInfoPartWidget *widget) { - return widget->key() == key; - }); - QVERIFY2(it != widgets.end(), qPrintable(key)); - (*it)->setText(context.value(key).toString().replace('\n', ' ')); - } - const int undo_count = diagram->undoStack().count(); - editor.apply(); - QCOMPARE(element->elementInformations().value("model").toString(), QString("RX-24")); - QCOMPARE(diagram->undoStack().count(), undo_count + 1); - diagram->undoStack().undo(); - QVERIFY(element->elementInformations().value("model").toString().isEmpty()); - diagram->undoStack().redo(); - QCOMPARE(element->elementInformations().value("voltage_rating").toString(), QString("24 V DC")); - QCOMPARE(DeviceBomExport::collect(project).first().m_device.m_model, QString("RX-24")); - editor.resize(600, 750); - editor.show(); - QCoreApplication::processEvents(); - const QString artifacts = qEnvironmentVariable("QET_SMART_DEVICE_ARTIFACT_DIR"); - if (!artifacts.isEmpty()) { - QVERIFY(editor.grab().save(QDir(artifacts).filePath("properties.png"))); - } - } - - void existingDatabaseReports() - { - QETProject project; - auto *element = addElement(project, project.addNewDiagram(), "-K1"); - QVERIFY(element); - element->setElementInformations(sample().toContext()); - project.dataBase()->updateDB(); - auto query = project.dataBase()->newQuery(QStringLiteral( - "SELECT model,category,voltage_rating,current_rating,notes FROM element_nomenclature_view")); - QVERIFY2(query.exec(), qPrintable(query.lastError().text())); - QVERIFY(query.next()); - QCOMPARE(query.value(0).toString(), sample().m_model); - QCOMPARE(query.value(1).toString(), sample().m_category); - QCOMPARE(query.value(2).toString(), sample().m_voltage_rating); - QCOMPARE(query.value(3).toString(), sample().m_current_rating); - QCOMPARE(query.value(4).toString(), sample().m_notes); - } + private slots: + void metadataColumns(); + void defaultQueryAndCsv(); + void atomicWrite(); }; -QTEST_MAIN(SmartDeviceTest) +void SmartDeviceTest::metadataColumns() +{ + const auto columns = BomExport::defaultColumns(); + for (const QString &column : { + QStringLiteral("label"), QStringLiteral("manufacturer"), + QStringLiteral("manufacturer_reference"), QStringLiteral("model"), + QStringLiteral("description"), QStringLiteral("category"), + QStringLiteral("quantity"), QStringLiteral("voltage_rating"), + QStringLiteral("current_rating"), QStringLiteral("folio"), + QStringLiteral("notes")}) + { + QVERIFY2(columns.contains(column), qPrintable(column)); + } +} + +void SmartDeviceTest::defaultQueryAndCsv() +{ + const QString connection = QStringLiteral("smart-device-%1") + .arg(QUuid::createUuid().toString()); + { + auto db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connection); + db.setDatabaseName(QStringLiteral(":memory:")); + QVERIFY2(db.open(), qPrintable(db.lastError().text())); + QSqlQuery setup(db); + QVERIFY(setup.exec(QStringLiteral( + "CREATE TABLE element_nomenclature_view (" + "label TEXT, designation TEXT, manufacturer TEXT, " + "manufacturer_reference TEXT, model TEXT, description TEXT, " + "category TEXT, quantity TEXT, voltage_rating TEXT, " + "current_rating TEXT, folio TEXT, notes TEXT, " + "element_type TEXT, diagram_position INTEGER, position INTEGER)"))); + + QSqlQuery insert(db); + insert.prepare(QStringLiteral( + "INSERT INTO element_nomenclature_view VALUES " + "(?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")); + auto add = [&insert](const QString &tag, const QString &type, int page, + int position, const QString &manufacturer) { + insert.bindValue(0, tag); + insert.bindValue(1, manufacturer); + insert.bindValue(2, QStringLiteral("0012")); + insert.bindValue(3, QStringLiteral("M-1")); + insert.bindValue(4, QStringLiteral("Power contactor")); + insert.bindValue(5, QStringLiteral("Control")); + insert.bindValue(6, QStringLiteral("1")); + insert.bindValue(7, QStringLiteral("24 V DC")); + insert.bindValue(8, QStringLiteral("16 A")); + insert.bindValue(9, QString::number(page)); + insert.bindValue(10, QStringLiteral("Quoted \"note\"\nnext line")); + insert.bindValue(11, type); + insert.bindValue(12, page); + insert.bindValue(13, position); + return insert.exec(); + }; + QVERIFY(add(QStringLiteral("-K2"), QStringLiteral("simple"), 2, 1, + QStringLiteral("日本電機"))); + QVERIFY(add(QStringLiteral("-K1"), QStringLiteral("master"), 1, 2, + QStringLiteral("Müller"))); + QVERIFY(add(QStringLiteral("-K1.1"), QStringLiteral("slave"), 1, 3, + QStringLiteral("Must not be exported"))); + QVERIFY(add(QStringLiteral("X1"), QStringLiteral("terminal"), 1, 4, + QStringLiteral("Must not be exported"))); + + QSqlQuery query(db); + QVERIFY2(query.exec(BomExport::defaultQuery()), + qPrintable(query.lastError().text())); + int rows = 0; + const auto csv = BomExport::toCsv( + query, BomExport::defaultColumns(), true, &rows); + QCOMPARE(rows, 2); + QVERIFY(csv.startsWith("\xEF\xBB\xBF\"label\";\"designation\";")); + QVERIFY(csv.contains(QStringLiteral("Müller").toUtf8())); + QVERIFY(csv.contains(QStringLiteral("日本電機").toUtf8())); + QVERIFY(csv.contains("\"Quoted \"\"note\"\"\nnext line\"")); + QVERIFY(!csv.contains("Must not be exported")); + QVERIFY(csv.indexOf("-K1") < csv.indexOf("-K2")); + } + QSqlDatabase::removeDatabase(connection); +} + +void SmartDeviceTest::atomicWrite() +{ + QTemporaryDir temporary; + QVERIFY(temporary.isValid()); + const auto path = temporary.filePath(QStringLiteral("bom.csv")); + const QByteArray csv("\xEF\xBB\xBF\"label\"\n"); + QString error = QStringLiteral("stale"); + QVERIFY(BomExport::writeCsv(path, csv, &error)); + QVERIFY(error.isEmpty()); + QFile file(path); + QVERIFY(file.open(QIODevice::ReadOnly)); + QCOMPARE(file.readAll(), csv); + QVERIFY(!BomExport::writeCsv( + temporary.filePath(QStringLiteral("missing/bom.csv")), csv, &error)); + QVERIFY(!error.isEmpty()); +} + +QTEST_GUILESS_MAIN(SmartDeviceTest) #include "tst_smart_device.moc"