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
+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/>.
*/
#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)
{
+7 -1
View File
@@ -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()
+21 -9
View File
@@ -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;
+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_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,
+5
View File
@@ -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";
+35 -66
View File
@@ -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 ; i<record_.count() ; ++i)
{
auto field_name = record_.fieldName(i);
qDebug() << "field name = " << field_name;
if (field_name == "position") {
header_name << tr("Position");
} else if (field_name == "diagram_position") {
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;
}
}
const auto field_name = record_.fieldName(i);
if (field_name == QLatin1String("position")) {
header_names << tr("Position");
} else if (field_name == QLatin1String("diagram_position")) {
header_names << tr("Position du folio");
} else if (field_name == QLatin1String("designation_qty")) {
header_names << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
const auto translated = QETInformation::translatedInfoKey(field_name);
header_names << (translated.isEmpty() ? field_name : translated);
}
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();
}
}
qDebug() << return_string;
return return_string;
return BomExport::toCsv(
query_, header_names, ui->m_include_headers->isChecked());
}
/**
+2 -1
View File
@@ -19,6 +19,7 @@
#define BOMEXPORTDIALOG_H
#include <QDialog>
#include <QByteArray>
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);
+1 -1
View File
@@ -36,7 +36,7 @@
<string>Formater en tant que liste de materiel</string>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
</widget>
</item>