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"