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 803aa6dee..3fdb11e08 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -177,6 +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/bomexport.cpp
+ ${QET_DIR}/sources/bomexport.h
${QET_DIR}/sources/conductorprofile.cpp
${QET_DIR}/sources/conductorprofile.h
${QET_DIR}/sources/conductorproperties.cpp
@@ -475,6 +477,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/project/projectusagetracker.cpp
${QET_DIR}/sources/project/projectusagetracker.h
+ ${QET_DIR}/sources/properties/deviceinformation.cpp
+ ${QET_DIR}/sources/properties/deviceinformation.h
${QET_DIR}/sources/properties/elementdata.cpp
${QET_DIR}/sources/properties/elementdata.h
${QET_DIR}/sources/properties/propertiesinterface.cpp
diff --git a/docs/smart-device-bom.md b/docs/smart-device-bom.md
new file mode 100644
index 000000000..d62823919
--- /dev/null
+++ b/docs/smart-device-bom.md
@@ -0,0 +1,93 @@
+# Smart Device Information and BOM CSV
+
+## Architecture
+
+- 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
+
+| 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 |
+
+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 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
+
+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`).
+
+The default columns are:
+
+`Device tag, Designation, Manufacturer, Part number, Model, Description,
+Category, Quantity, Voltage rating, Current rating, Page, Notes`.
+
+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
+
+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:
+
+```sh
+cmake -S . -B build -DPACKAGE_TESTS=ON
+cmake --build build
+ctest --test-dir build --output-on-failure
+```
+
+`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 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.
+
+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/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/bomexport.h b/sources/bomexport.h
new file mode 100644
index 000000000..0241165a0
--- /dev/null
+++ b/sources/bomexport.h
@@ -0,0 +1,37 @@
+/*
+ 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 .
+*/
+#ifndef BOMEXPORT_H
+#define BOMEXPORT_H
+
+#include
+#include
+#include
+
+class QSqlQuery;
+
+namespace BomExport
+{
+ 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);
+}
+
+#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 08286b6ba..1549f6ce0 100644
--- a/sources/dataBase/projectdatabase.cpp
+++ b/sources/dataBase/projectdatabase.cpp
@@ -600,6 +600,11 @@ void projectDataBase::createElementNomenclatureView()
"ei.designation AS designation,"
"ei.manufacturer AS manufacturer,"
"ei.manufacturer_reference AS manufacturer_reference,"
+ "ei.model AS model,"
+ "ei.category AS category,"
+ "ei.voltage_rating AS voltage_rating,"
+ "ei.current_rating AS current_rating,"
+ "ei.notes AS notes,"
"ei.machine_manufacturer_reference AS machine_manufacturer_reference,"
"ei.supplier AS supplier,"
"ei.quantity AS quantity,"
@@ -658,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/properties/deviceinformation.cpp b/sources/properties/deviceinformation.cpp
new file mode 100644
index 000000000..80010087a
--- /dev/null
+++ b/sources/properties/deviceinformation.cpp
@@ -0,0 +1,65 @@
+/*
+ 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 "deviceinformation.h"
+
+#include "../qetinformation.h"
+
+namespace {
+struct DeviceField
+{
+ QString m_key;
+ QString DeviceInformation::*m_member;
+};
+
+const DeviceField fields[] = {
+ {QETInformation::ELMT_LABEL, &DeviceInformation::m_device_tag},
+ {QETInformation::ELMT_MANUFACTURER, &DeviceInformation::m_manufacturer},
+ {QETInformation::ELMT_MANUFACTURER_REF, &DeviceInformation::m_part_number},
+ {QETInformation::ELMT_MODEL, &DeviceInformation::m_model},
+ {QETInformation::ELMT_DESCRIPTION, &DeviceInformation::m_description},
+ {QETInformation::ELMT_CATEGORY, &DeviceInformation::m_category},
+ {QETInformation::ELMT_QUANTITY, &DeviceInformation::m_quantity},
+ {QETInformation::ELMT_VOLTAGE_RATING, &DeviceInformation::m_voltage_rating},
+ {QETInformation::ELMT_CURRENT_RATING, &DeviceInformation::m_current_rating},
+ {QETInformation::ELMT_NOTES, &DeviceInformation::m_notes}
+};
+}
+
+DeviceInformation DeviceInformation::fromContext(const DiagramContext &context)
+{
+ DeviceInformation info;
+ for (const auto &field : fields) {
+ info.*(field.m_member) = context.value(field.m_key).toString();
+ }
+ return info;
+}
+
+DiagramContext DeviceInformation::toContext(DiagramContext context) const
+{
+ for (const auto &field : fields)
+ {
+ const auto &value = this->*(field.m_member);
+ if (value.isEmpty()) {
+ context.remove(field.m_key);
+ } else {
+ const bool show = !context.contains(field.m_key) || context.keyMustShow(field.m_key);
+ context.addValue(field.m_key, value, show);
+ }
+ }
+ return context;
+}
diff --git a/sources/properties/deviceinformation.h b/sources/properties/deviceinformation.h
new file mode 100644
index 000000000..6e355bb9b
--- /dev/null
+++ b/sources/properties/deviceinformation.h
@@ -0,0 +1,47 @@
+/*
+ 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 .
+*/
+#ifndef DEVICEINFORMATION_H
+#define DEVICEINFORMATION_H
+
+#include "../diagramcontext.h"
+
+#include
+
+/**
+ * Device properties backed by ElementData::m_informations, not a second store.
+ * Missing properties remain empty. Quantity and ratings retain the existing
+ * free-text representation (including decimal quantities and units).
+ */
+struct DeviceInformation
+{
+ QString m_device_tag;
+ QString m_manufacturer;
+ QString m_part_number;
+ QString m_model;
+ QString m_description;
+ QString m_category;
+ QString m_quantity;
+ QString m_voltage_rating;
+ QString m_current_rating;
+ QString m_notes;
+
+ static DeviceInformation fromContext(const DiagramContext &context);
+ DiagramContext toContext(DiagramContext context = DiagramContext()) const;
+};
+
+#endif // DEVICEINFORMATION_H
diff --git a/sources/qetinformation.cpp b/sources/qetinformation.cpp
index 05a246d47..8da329bbf 100644
--- a/sources/qetinformation.cpp
+++ b/sources/qetinformation.cpp
@@ -150,6 +150,11 @@ QStringList QETInformation::elementInfoKeys()
ELMT_DESIGNATION,
ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF,
+ ELMT_MODEL,
+ ELMT_CATEGORY,
+ ELMT_VOLTAGE_RATING,
+ ELMT_CURRENT_RATING,
+ ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER,
ELMT_QUANTITY,
@@ -234,7 +239,12 @@ QString QETInformation::infoToVar(const QString &info)
*/
QString QETInformation::translatedInfoKey(const QString &info)
{
- if (info == DIA_AUTHOR) return QObject::tr("Auteur");
+ if (info == ELMT_MODEL) return QObject::tr("Modèle");
+ else if (info == ELMT_CATEGORY) return QObject::tr("Catégorie");
+ else if (info == ELMT_VOLTAGE_RATING) return QObject::tr("Tension nominale");
+ else if (info == ELMT_CURRENT_RATING) return QObject::tr("Courant nominal");
+ else if (info == ELMT_NOTES) return QObject::tr("Notes");
+ else if (info == DIA_AUTHOR) return QObject::tr("Auteur");
else if (info == DIA_DATE) return QObject::tr("Date");
else if (info == DIA_TITLE) return QObject::tr("Titre");
else if (info == DIA_FILENAME) return QObject::tr("Fichier");
@@ -332,6 +342,11 @@ QStringList QETInformation::elementEditorElementInfoKeys()
ELMT_DESIGNATION,
ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF,
+ ELMT_MODEL,
+ ELMT_CATEGORY,
+ ELMT_VOLTAGE_RATING,
+ ELMT_CURRENT_RATING,
+ ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER,
ELMT_QUANTITY,
diff --git a/sources/qetinformation.h b/sources/qetinformation.h
index 9fee0cdf3..ee144a301 100644
--- a/sources/qetinformation.h
+++ b/sources/qetinformation.h
@@ -44,6 +44,11 @@ namespace QETInformation
static QString ELMT_MACHINE_MANUFACTURER_REF = "machine_manufacturer_reference";
static QString ELMT_SUPPLIER = "supplier";
static QString ELMT_QUANTITY = "quantity";
+ static QString ELMT_MODEL = "model";
+ static QString ELMT_CATEGORY = "category";
+ static QString ELMT_VOLTAGE_RATING = "voltage_rating";
+ static QString ELMT_CURRENT_RATING = "current_rating";
+ static QString ELMT_NOTES = "notes";
static QString ELMT_UNITY = "unity";
static QString ELMT_PLANT = "plant";
static QString ELMT_LOCATION = "location";
diff --git a/sources/ui/bomexportdialog.cpp b/sources/ui/bomexportdialog.cpp
index 63429ede7..e2ea6ce77 100644
--- a/sources/ui/bomexportdialog.cpp
+++ b/sources/ui/bomexportdialog.cpp
@@ -18,6 +18,7 @@
#include "bomexportdialog.h"
#include "../dataBase/ui/elementquerywidget.h"
+#include "../bomexport.h"
#include "../qetapp.h"
#include "../qetinformation.h"
#include "../qetproject.h"
@@ -41,8 +42,8 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_query_widget = new ElementQueryWidget(this);
ui->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/tst_smart_device.cpp b/tests/qttest/tst_smart_device.cpp
new file mode 100644
index 000000000..b054a6eba
--- /dev/null
+++ b/tests/qttest/tst_smart_device.cpp
@@ -0,0 +1,135 @@
+/*
+ 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
+#include
+#include
+#include
+
+class SmartDeviceTest : public QObject
+{
+ Q_OBJECT
+
+ private slots:
+ void metadataColumns();
+ void defaultQueryAndCsv();
+ void atomicWrite();
+};
+
+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"