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"