Merge pull request #830 from enesgursoy6110/feature/smart-device-bom

Add smart device metadata to the existing BOM export
This commit is contained in:
Laurent Trinques
2026-09-11 23:25:04 +02:00
committed by GitHub
17 changed files with 612 additions and 120 deletions
+1
View File
@@ -134,6 +134,7 @@ include(cmake/fetch_kdeaddons.cmake)
option(PACKAGE_TESTS "Build the tests" ON) option(PACKAGE_TESTS "Build the tests" ON)
if(PACKAGE_TESTS) if(PACKAGE_TESTS)
message("Add sub directory tests") message("Add sub directory tests")
enable_testing()
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
+4
View File
@@ -177,6 +177,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/wiringlistdialog.h ${QET_DIR}/sources/ui/wiringlistdialog.h
${QET_DIR}/sources/ui/wiringlistdialog.cpp ${QET_DIR}/sources/ui/wiringlistdialog.cpp
${QET_DIR}/sources/conductornumexport.h ${QET_DIR}/sources/conductornumexport.h
${QET_DIR}/sources/bomexport.cpp
${QET_DIR}/sources/bomexport.h
${QET_DIR}/sources/conductorprofile.cpp ${QET_DIR}/sources/conductorprofile.cpp
${QET_DIR}/sources/conductorprofile.h ${QET_DIR}/sources/conductorprofile.h
${QET_DIR}/sources/conductorproperties.cpp ${QET_DIR}/sources/conductorproperties.cpp
@@ -475,6 +477,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/project/projectusagetracker.cpp ${QET_DIR}/sources/project/projectusagetracker.cpp
${QET_DIR}/sources/project/projectusagetracker.h ${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.cpp
${QET_DIR}/sources/properties/elementdata.h ${QET_DIR}/sources/properties/elementdata.h
${QET_DIR}/sources/properties/propertiesinterface.cpp ${QET_DIR}/sources/properties/propertiesinterface.cpp
+93
View File
@@ -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.
+113
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "bomexport.h"
#include <QSaveFile>
#include <QSqlQuery>
#include <QSqlRecord>
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;
}
+37
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef BOMEXPORT_H
#define BOMEXPORT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
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
+21 -41
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>. along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "cli_export.h" #include "cli_export.h"
#include "bomexport.h"
#include "bordertitleblock.h" #include "bordertitleblock.h"
#include "conductornumexport.h" #include "conductornumexport.h"
@@ -304,61 +305,40 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
return 0; return 0;
} }
/// Quote a field for CSV output (RFC-4180 style, ';' delimiter). /// Bill of materials from the same project database and default query as the
QString csvField(const QString &value) /// GUI nomenclature export.
{
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.
int exportBom(QETProject &project, const QString &output) int exportBom(QETProject &project, const QString &output)
{ {
// The project database is built lazily; force a (re)build before querying.
project.dataBase()->updateDB(); project.dataBase()->updateDB();
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
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");
if (!query.exec()) { if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n"; err << "BOM query failed: " << query.lastError().text() << "\n";
return 1; return 1;
} }
QString csv = columns.join(";") % "\n";
int rows = 0; int rows = 0;
while (query.next()) { const auto csv = BomExport::toCsv(
QStringList values; query, BomExport::defaultColumns(), true, &rows);
for (int i = 0; i < columns.size(); ++i) QString error;
values << csvField(query.value(i).toString()); if (!BomExport::writeCsv(output, csv, &error)) {
csv += values.join(";") % "\n"; err << "Cannot write '" << output << "': " << error << "\n";
++rows;
}
QFile file(output);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
err << "Cannot open '" << output << "' for writing.\n";
return 1; return 1;
} }
QTextStream fout(&file);
fout << csv;
file.close();
out << "Exported " << rows << " component(s) -> " << output << "\n"; out << "Exported " << rows << " component(s) -> " << output << "\n";
return 0; 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. /// Count terminals on @p element that no conductor connects to.
int freeTerminals(Element *element) int freeTerminals(Element *element)
{ {
+7 -1
View File
@@ -600,6 +600,11 @@ void projectDataBase::createElementNomenclatureView()
"ei.designation AS designation," "ei.designation AS designation,"
"ei.manufacturer AS manufacturer," "ei.manufacturer AS manufacturer,"
"ei.manufacturer_reference AS manufacturer_reference," "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.machine_manufacturer_reference AS machine_manufacturer_reference,"
"ei.supplier AS supplier," "ei.supplier AS supplier,"
"ei.quantity AS quantity," "ei.quantity AS quantity,"
@@ -658,7 +663,8 @@ void projectDataBase::createElementNomenclatureView()
"di.folio AS folio," "di.folio AS folio,"
"e.pos AS position " "e.pos AS position "
" FROM element_info ei, diagram_info di, element e, diagram d" " 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 //The element table holds every element of the project; which
//kinds belong in a nomenclature is this view's business, not //kinds belong in a nomenclature is this view's business, not
//the table's. Kept identical to the mask populateElementTable() //the table's. Kept identical to the mask populateElementTable()
+21 -9
View File
@@ -160,6 +160,8 @@ void ElementQueryWidget::setQuery(const QString &query)
{ {
auto str_type = rxm.captured(1); auto str_type = rxm.captured(1);
where.remove(str_type); where.remove(str_type);
const bool all_master_types = str_type.contains(
ElementData::typeToString(ElementData::Master));
int c=0; int c=0;
ui->m_simple_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Simple)) ? true : false); 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()) { if (ui->m_terminal_cb->isChecked()) {
++c; ++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()) { if (ui->m_coil_cb->isChecked()) {
++c; ++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()) { if (ui->m_button_cb->isChecked()) {
++c; ++c;
} }
ui->m_protection_cb->setChecked(str_type.contains(ElementData::masterTypeToString(ElementData::Protection)) ? true : false); ui->m_protection_cb->setChecked(all_master_types || str_type.contains(ElementData::masterTypeToString(ElementData::Protection)));
if (ui->m_protection_cb) { if (ui->m_protection_cb->isChecked()) {
++c; ++c;
} }
ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false); ui->m_thumbnail_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Thumbnail)) ? true : false);
if (ui->m_thumbnail_cb->isChecked()) { if (ui->m_thumbnail_cb->isChecked()) {
++c; ++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()) { if (ui->m_plc_cb->isChecked()) {
++c; ++c;
} }
@@ -352,22 +354,32 @@ QString ElementQueryWidget::queryStr() const
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'"; where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Simple) += "'";
b = true; 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"; if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Commutator) += "'";
b = true; b = true;
} }
if (ui->m_coil_cb->isChecked()) { if (!all_master_types && ui->m_coil_cb->isChecked()) {
if (b) where +=" OR"; if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Coil) += "'";
b = true; b = true;
} }
if (ui->m_protection_cb->isChecked()) { if (!all_master_types && ui->m_protection_cb->isChecked()) {
if (b) where +=" OR"; if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::Protection) += "'";
b = true; b = true;
} }
if (ui->m_plc_cb->isChecked()) { if (!all_master_types && ui->m_plc_cb->isChecked()) {
if (b) where +=" OR"; if (b) where +=" OR";
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'"; where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true; b = true;
+65
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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;
}
+47
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICEINFORMATION_H
#define DEVICEINFORMATION_H
#include "../diagramcontext.h"
#include <QString>
/**
* 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
+16 -1
View File
@@ -150,6 +150,11 @@ QStringList QETInformation::elementInfoKeys()
ELMT_DESIGNATION, ELMT_DESIGNATION,
ELMT_MANUFACTURER, ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF, ELMT_MANUFACTURER_REF,
ELMT_MODEL,
ELMT_CATEGORY,
ELMT_VOLTAGE_RATING,
ELMT_CURRENT_RATING,
ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF, ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER, ELMT_SUPPLIER,
ELMT_QUANTITY, ELMT_QUANTITY,
@@ -234,7 +239,12 @@ QString QETInformation::infoToVar(const QString &info)
*/ */
QString QETInformation::translatedInfoKey(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_DATE) return QObject::tr("Date");
else if (info == DIA_TITLE) return QObject::tr("Titre"); else if (info == DIA_TITLE) return QObject::tr("Titre");
else if (info == DIA_FILENAME) return QObject::tr("Fichier"); else if (info == DIA_FILENAME) return QObject::tr("Fichier");
@@ -332,6 +342,11 @@ QStringList QETInformation::elementEditorElementInfoKeys()
ELMT_DESIGNATION, ELMT_DESIGNATION,
ELMT_MANUFACTURER, ELMT_MANUFACTURER,
ELMT_MANUFACTURER_REF, ELMT_MANUFACTURER_REF,
ELMT_MODEL,
ELMT_CATEGORY,
ELMT_VOLTAGE_RATING,
ELMT_CURRENT_RATING,
ELMT_NOTES,
ELMT_MACHINE_MANUFACTURER_REF, ELMT_MACHINE_MANUFACTURER_REF,
ELMT_SUPPLIER, ELMT_SUPPLIER,
ELMT_QUANTITY, ELMT_QUANTITY,
+5
View File
@@ -44,6 +44,11 @@ namespace QETInformation
static QString ELMT_MACHINE_MANUFACTURER_REF = "machine_manufacturer_reference"; static QString ELMT_MACHINE_MANUFACTURER_REF = "machine_manufacturer_reference";
static QString ELMT_SUPPLIER = "supplier"; static QString ELMT_SUPPLIER = "supplier";
static QString ELMT_QUANTITY = "quantity"; 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_UNITY = "unity";
static QString ELMT_PLANT = "plant"; static QString ELMT_PLANT = "plant";
static QString ELMT_LOCATION = "location"; static QString ELMT_LOCATION = "location";
+35 -66
View File
@@ -18,6 +18,7 @@
#include "bomexportdialog.h" #include "bomexportdialog.h"
#include "../dataBase/ui/elementquerywidget.h" #include "../dataBase/ui/elementquerywidget.h"
#include "../bomexport.h"
#include "../qetapp.h" #include "../qetapp.h"
#include "../qetinformation.h" #include "../qetinformation.h"
#include "../qetproject.h" #include "../qetproject.h"
@@ -41,8 +42,8 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_query_widget = new ElementQueryWidget(this); m_query_widget = new ElementQueryWidget(this);
ui->m_main_layout->insertWidget(0, m_query_widget); ui->m_main_layout->insertWidget(0, m_query_widget);
//By default format as bom is clicked m_query_widget->setQuery(BomExport::defaultQuery());
on_m_format_as_bom_clicked(true); on_m_format_as_bom_clicked(false);
} }
/** /**
@@ -67,90 +68,58 @@ int BOMExportDialog::exec()
if (dir.isEmpty()) dir = QETApp::documentDir(); if (dir.isEmpty()) dir = QETApp::documentDir();
QString file_name = dir % "/" % tr("nomenclature_") % QString(m_project ->title() % ".csv"); 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)")); QString file_path = QFileDialog::getSaveFileName(this, tr("Enregister sous... "), file_name, tr("Fichiers csv (*.csv)"));
QFile file(file_path);
if (!file_path.isEmpty()) if (!file_path.isEmpty())
{ {
if (QFile::exists(file_path )) QString error;
{ const auto csv = getBom(&error);
// if file already exist -> delete it if (!error.isEmpty() || !BomExport::writeCsv(file_path, csv, &error)) {
if (!QFile::remove(file_path) ) QMessageBox::critical(
{ this, tr("Erreur"),
QMessageBox::critical(this, tr("Erreur"), tr("Impossible d'enregistrer la nomenclature dans %1.\n%2")
tr("Impossible de remplacer le fichier!\n\n")+ .arg(file_path, error));
"Destination : "+file_path+"\n");
}
}
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
stream << getBom() << &Qt::endl(stream);
} }
} }
} }
return r; return r;
} }
QString BOMExportDialog::getBom() QByteArray BOMExportDialog::getBom(QString *error)
{ {
if (error) {
error->clear();
}
m_project->dataBase()->updateDB(); m_project->dataBase()->updateDB();
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr()); auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr());
QString return_string;
if (!query_.exec()) { if (!query_.exec()) {
qDebug() << "BOMExportDialog::getBom : query errir : " << query_.lastError(); 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 const auto record_ = query_.record();
if (ui->m_include_headers) for (int i = 0; i < record_.count(); ++i)
{ {
auto record_ = query_.record(); const auto field_name = record_.fieldName(i);
QStringList header_name; if (field_name == QLatin1String("position")) {
for (auto i=0 ; i<record_.count() ; ++i) header_names << tr("Position");
{ } else if (field_name == QLatin1String("diagram_position")) {
auto field_name = record_.fieldName(i); header_names << tr("Position du folio");
} else if (field_name == QLatin1String("designation_qty")) {
qDebug() << "field name = " << field_name; header_names << tr("Quantité numéro d'article", "Special field with name : designation quantity");
if (field_name == "position") { } else {
header_name << tr("Position"); const auto translated = QETInformation::translatedInfoKey(field_name);
} else if (field_name == "diagram_position") { header_names << (translated.isEmpty() ? field_name : translated);
header_name << tr("Position du folio");
} else if (field_name == "designation_qty") {
header_name << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
header_name << QETInformation::translatedInfoKey(field_name);
if (header_name.isEmpty()) {
header_name << field_name;
}
}
} }
return_string = header_name.join(";") % "\n";
}
//ROWS
while (query_.next())
{
auto i=0;
QStringList values;
while (query_.value(i).isValid())
{
auto date = query_.value(i).toDate();
if (!date.isNull()) {
values << QLocale::system().toString(query_.value(i).toDate(), QLocale::ShortFormat);
} else {
values << query_.value(i).toString();
}
++i;
}
return_string += values.join(";") % "\n";
values.clear();
} }
} }
return BomExport::toCsv(
qDebug() << return_string; query_, header_names, ui->m_include_headers->isChecked());
return return_string;
} }
/** /**
+2 -1
View File
@@ -19,6 +19,7 @@
#define BOMEXPORTDIALOG_H #define BOMEXPORTDIALOG_H
#include <QDialog> #include <QDialog>
#include <QByteArray>
class QETProject; class QETProject;
class ElementQueryWidget; class ElementQueryWidget;
@@ -39,7 +40,7 @@ class BOMExportDialog : public QDialog
~BOMExportDialog() override; ~BOMExportDialog() override;
virtual int exec() override; virtual int exec() override;
QString getBom(); QByteArray getBom(QString *error = nullptr);
private slots: private slots:
void on_m_format_as_bom_clicked(bool checked); void on_m_format_as_bom_clicked(bool checked);
+1 -1
View File
@@ -36,7 +36,7 @@
<string>Formater en tant que liste de materiel</string> <string>Formater en tant que liste de materiel</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
+9
View File
@@ -91,3 +91,12 @@ add_executable(tst_contactusage tst_contactusage.cpp)
add_test(NAME tst_contactusage COMMAND tst_contactusage) add_test(NAME tst_contactusage COMMAND tst_contactusage)
target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources) target_include_directories(tst_contactusage PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_contactusage PRIVATE Qt::Test) 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)
+135
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "bomexport.h"
#include <QFile>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QTemporaryDir>
#include <QtTest>
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"