Compare commits

...

9 Commits

Author SHA1 Message Date
Laurent Trinques f85ee19fc0 Merge pull request #830 from enesgursoy6110/feature/smart-device-bom
Add smart device metadata to the existing BOM export
2026-09-11 23:25:04 +02:00
Laurent Trinques 2f415117f7 Merge pull request #837 from ispyisail/fix/prefix-nesting-and-multitree
Fix element prefix lookup: nesting-aware matching, multi-tree common collections (#671 items 2, 5)
2026-09-11 23:24:42 +02:00
ispyisail 7c0e867226 Rewrite element-prefix lookup for nesting and multi-tree common collections
The remaining two defects from bugtracker #671's original analysis,
which #686 knowingly didn't cover (see that PR's review thread and the
comment on the now-closed #672).

## #671 item 5: the XML matching ignored nesting

prefixFromLabelFile() was a flat token scan: it matched any <category
name="..."> whose name equalled the next path segment, with no check
that the match was actually a *child* of the previous match. It gave
correct results on the shipped 10_electric/qet_labels.xml only because
that file's document order happens to line up with its hierarchy --
any file with a same-named category at the wrong nesting depth would
silently return the wrong prefix.

Reproduced with a synthetic file where a top-level sibling category
happens to share a name with what should be an unmatched grandchild:
the old (already re-verified-fixed-for-whitespace) lookup returns a
prefix from a completely unrelated branch of the document; this
rewrite correctly reports "not found".

Fixed by replacing the QXmlStreamReader token walk with a QDomDocument
walk that only ever considers a matched node's direct <category>
children (firstChildElement()/nextSiblingElement(), scoped to that
node), which cannot cross into a same-named sibling subtree. This also
makes the whitespace-dependence fixed in #686 moot for the same
reason: DOM parsing doesn't distinguish pretty-printed from minified
input to begin with.

The inheritance rule ("if a directory has no prefix, use its parent's,
and so on") and the empty-<prefix/>-overrides-inheritance behaviour
#686 added both carry over unchanged: a category's own <prefix> child,
even an empty one, always overrides whatever a shallower ancestor
already provided; a category with no <prefix> child at all leaves the
inherited value untouched.

## #671 item 2: common-collection trees other than 10_electric

The lookup only ever consulted commonElementsDir()/10_electric --
literally: `if (current_location.fileName() == "10_electric")`. The
common collection ships four other top-level trees (20_logic,
30_hydraulic, 50_pneumatic, 60_energy); none of them could carry a
qet_labels.xml at all, because nothing ever looked for one.

Generalised to commonElementsDir()/<tree>/qet_labels.xml for whichever
top-level tree the element's path actually walks up to, tried first,
then custom, then company -- each of the latter two tried against both
a from-root layout (matching a custom/company file organised as a
mirror of the common collection, tree name included) and a
tree-relative one (matching a file scoped to just one tree), so
existing custom files keep working either way. This is the same
multi-candidate structure #686 already established for custom-then-
company; it now also covers which common-collection tree to check.

## Testing

Same constraint as #686: no working full build in this sandbox
(missing generated headers/deps), so the exact functions as committed
were extracted into a standalone Qt6 harness and run against the real
shipped 10_electric/qet_labels.xml (pretty-printed and minified),
a synthetic empty-prefix-override file, and the nesting-trap file
above -- 9/9, including the three cases #686 already fixed (direct
prefix, inherited prefix, not-found) staying correct, confirming this
rewrite doesn't regress that work.

Not exercised here (needs a real running QETApp / ElementsLocation,
which the standalone harness can't stand up): the elementPrefixForLocation()
candidate-list wiring itself -- collection_root computation, the
from-root/tree-relative dual lookup, and the common-then-custom-then-
company ordering. That code is mechanical and was reviewed carefully
by hand, but it has not been run.
2026-09-12 06:32:07 +12:00
ispyisail 0b7197118a Fix three follow-on defects in the prefix lookup this PR just refactored
Requested by @scorpio810 in review: an empty <prefix/> in the custom
collection should cancel a company-collection prefix, not fall through
to it. QXmlStreamReader::readElementText() returns a null QString for an
empty element, and the caller's isNull() check treats that the same as
"not found" -- distinguish the two so an explicit override actually
overrides. Verified in a standalone harness against a synthetic
override file, pretty-printed and minified.

Two more while in the same function, both from the original bugtracker
#671 analysis that this PR only partially addressed:

- QString path[10] with an unbounded index becomes a QStringList. The
  deepest category in the shipped collection already needs 9 of the 10
  slots; a custom collection can nest deeper, and overflow was writing
  QString objects past the end of a stack array (#671 item 3).
- The common-collection lookup still concatenated
  commonElementsDir() + "10_electric/qet_labels.xml" directly.
  commonElementsDir() returns the configured path verbatim with no
  guaranteed trailing separator, so relocating the collection to a path
  without one silently mangles this into one word and the file is never
  found -- the single most-reported cause of "prefixes don't work"
  (#671 item 1, forum #2178/#2651). QDir::filePath() joins correctly
  either way; applied to all three lookups (common, custom, company).

Also fixes a defect not in that original analysis: the token-matching
loop in prefixFromLabelFile() advanced twice per matched element --
once explicitly after a match, once more unconditionally at the bottom
of the loop -- which only produced the right result because a
pretty-printed file inserts a whitespace Characters token between
adjacent elements for the second advance to land on. A minified
qet_labels.xml has no such token, so the second advance skips clean
over the very element being searched for and the lookup silently finds
nothing -- reproduced against the real shipped 10_electric/qet_labels.xml
(returns "" instead of "K" for a plain coil, on every case tested, not
just the inheritance one). A single `continue` after a handled match
removes the double advance.

Testing: extracted the exact functions as committed into a standalone
Qt6 harness (outside the full QET build, which needs a dependency
fetch this sandbox doesn't have) and ran them against the real shipped
qet_labels.xml, pretty-printed and minified, covering a direct prefix,
inherited-from-ancestor prefix, not-found, and the explicit-empty-
override case -- 8/8, matching between formats, no regressions in the
pretty-printed results. The QDir::filePath() fix was verified
separately against both a trailing-slash and no-trailing-slash base
path. Not yet built inside the actual application (pugixml and other
generated headers aren't available standalone); the algorithm itself,
which is where all four defects lived, is what was under test.
2026-09-12 06:22:26 +12:00
enesgursoy6110 77f8e26ffc Address smart device BOM review feedback 2026-09-11 20:12:09 +03:00
enesgursoy6110 16dbc5c404 Add ungrouped device BOM CSV export 2026-09-11 20:11:28 +03:00
enesgursoy6110 73f7ea8f50 Add smart device metadata to element information 2026-09-11 20:11:19 +03:00
Laurent Trinques 99e9ce5aca QETApp: fall back to English when the QET translation is empty
A .qm compiled from a 0%-translated .ts (fi, no, rs, sk, sl, sr) loads
successfully but contains no messages, so setLanguage() treated the
language as loaded and never fell back to qet_en: users got the French
source strings instead of English. Treat an empty translator as not
loaded.

Also log the QET and Qt .qm files actually loaded in the startup
diagnostics (MachineInfo), to make translation reports easier to triage.
2026-09-11 16:28:11 +02:00
Levi Jetzer e7cbb8f50e Reading prefixes from company collection
Added a reading for company collection prefixes which are overwritten by user collection prefixes
2026-08-07 17:20:45 +02:00
21 changed files with 785 additions and 221 deletions
+1
View File
@@ -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()
+4
View File
@@ -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
+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.
+139 -98
View File
@@ -24,6 +24,8 @@
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
#include <QDir>
#include <QDomDocument>
#include <QStringList>
#include <QVariant>
#include <utility>
@@ -703,6 +705,74 @@ namespace autonum
return formula;
}
/**
@brief prefixFromLabelFile
Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
deepest directory; path[0], the element's own file name, is never
matched) in the qet_labels.xml at @a filepath.
Descends through nested \<category name="..."\> elements matching
path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
only *direct* children at each step -- unlike a flat token scan,
this cannot be fooled by a same-named category living elsewhere in
the document at the wrong nesting depth (bugtracker #671 item 5).
At each matched level, that category's own \<prefix\> child -- even
an empty one -- overrides whatever a shallower ancestor already
provided, so an explicit empty \<prefix/\> cancels inheritance
rather than silently falling back to it (the behaviour requested in
PR #686 review). A category with no \<prefix\> child at all leaves
the inherited value untouched, which is how a directory with no
prefix of its own comes to inherit its parent's, as the file's own
header comment documents.
@return the prefix that applies, or a null QString if the file
cannot be read, is not well-formed, or does not describe this
path at all (as opposed to describing it with no prefix
anywhere along it, which is a non-null empty string).
*/
static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
{
QFile file(filepath);
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
QDomDocument document;
if (!document.setContent(&file))
return QString();
QDomElement node = document.documentElement();
if (node.isNull())
return QString();
QString prefix;
for (int i = dirLevel ; i >= 1 ; --i) {
QDomElement child = node.firstChildElement(QStringLiteral("category"));
while (!child.isNull()
&& child.attribute(QStringLiteral("name")) != path[i]) {
child = child.nextSiblingElement(QStringLiteral("category"));
}
if (child.isNull())
return QString();
node = child;
const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
if (!own.isNull()) {
//readElementText()'s null-vs-empty distinction that PR
//#686 needed for the old QXmlStreamReader-based lookup
//has a QDomElement equivalent: text() on an empty
//element can itself come back null depending on how the
//XML was written, so the same explicit fallback applies
//-- an empty QString here means "found, deliberately
//blank", not "not found".
prefix = own.text();
if (prefix.isNull())
prefix = QString("");
}
}
return prefix;
}
/**
@brief elementPrefixForLocation
@param location
@@ -716,114 +786,85 @@ namespace autonum
if (!location.isProject())
return QString();
QXmlStreamReader rxml;
QString path[10];
int i = -1;
//Directory names from the element up to (not including) the
//collection root, outermost last -- path[dirLevel] is the
//top-level category, path[1] the element's immediate parent
//directory, path[0] the element's own file name (never matched
//against a category: the search stops descending once it has
//matched path[1], the deepest real directory). An unbounded
//QStringList rather than a fixed-size array, because a custom
//collection can nest deeper than the shipped one -- see
//bugtracker #671 item 3.
QStringList path;
ElementsLocation current_location = location;
int dirLevel = -1;
//Add location name to path array
while((current_location.parent() != current_location) && (current_location.parent().fileName() != "import"))
while ((current_location.parent() != current_location)
&& (current_location.parent().fileName() != "import"))
{
i++;
path[i]=current_location.fileName();
path << current_location.fileName();
current_location = current_location.parent();
dirLevel++;
}
//User Element without folder treatment
if (i == -1)
{
i = 0;
path[i]=current_location.fileName();
//User element without folder treatment
if (path.isEmpty()) {
path << current_location.fileName();
current_location = current_location.parent();
dirLevel = 0;
}
const int dirLevel = path.size() - 1;
//Name of the top-level tree the element's path was found
//under, e.g. "10_electric" -- or, for a custom/company
//collection not organised that way, whatever its top-level
//folder happens to be called.
const QString collection_root = current_location.fileName();
//Every top-level common-collection tree (10_electric,
//20_logic, 30_hydraulic, ...) may carry its own
//qet_labels.xml, with categories relative to that tree, the
//same way 10_electric/qet_labels.xml already does -- not just
//10_electric, which is all the hardcoded check this replaces
//used to allow (bugtracker #671 item 2). commonElementsDir()
//-- unlike customElementsDir(), which normalises this itself
//-- returns whatever path the user configured verbatim, with
//no guaranteed trailing separator; concatenating a suffix onto
//it directly used to silently mangle the path (and so the
//prefix lookup) for any install relocated to a directory
//without a trailing slash (#671 item 1). QDir::filePath()
//joins correctly either way.
{
const QString common_file = QDir(QETApp::commonElementsDir())
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
if (!prefix.isNull()) {
return prefix;
}
}
// Create Custom labels if qet_labels.xml exits in customElementsDir
if (current_location.fileName() != "10_electric"){
QString custom_labels = "qet_labels.xml";
QString customfilepath = QETApp::customElementsDir().append(custom_labels);
QFile file(customfilepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
/* Which collection an element actually came from is not
* recoverable post-import (addElement() strips the protocol),
* so custom and company labels files are tried against two
* possible layouts: with the collection's top-level tree name
* folded into the path (a custom/company file organised as a
* mirror of the common collection, tree name included) and
* without it (a file scoped to just this one tree, matching
* how the common collection's own files are written). Custom
* is tried before company, so a user override wins over a
* shared one.
*/
QStringList path_from_root = path;
path_from_root << collection_root;
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
const QStringList candidate_dirs = {
QETApp::customElementsDir(),
QETApp::companyElementsDir()
};
for (const QString &dir : candidate_dirs) {
const QString candidate =
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
for (const QStringList &segments : {path_from_root, path}) {
const QString prefix = prefixFromLabelFile(
candidate, segments, segments.size() - 1);
if (!prefix.isNull()) {
return prefix;
}
}
rxml.readNext();
}
}
else
{
QString qet_labels = "10_electric/qet_labels.xml";
QString filepath = QETApp::commonElementsDir().append(qet_labels);
QFile file(filepath);
file.isReadable();
if (!file.open(QFile::ReadOnly | QFile::Text))
return QString();
rxml.setDevice(&file);
rxml.readNext();
while(!rxml.atEnd())
{
if (rxml.attributes().value("name").toString() == path[i])
{
rxml.readNext();
i=i-1;
//reached element directory
if (i==0)
{
for (int j=i; j<= dirLevel; j = j +1)
{
//if there is a prefix available apply prefix
if(rxml.name().toString()=="prefix")
{
return rxml.readElementText();
}
//if there isn't a prefix available, find parent prefix in parent folder
else
{
while (rxml.readNextStartElement() && rxml.name().toString()!="prefix")
{
rxml.skipCurrentElement();
rxml.readNext();
}
}
}
}
}
rxml.readNext();
}
}
return QString();
}
+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;
+2
View File
@@ -159,6 +159,8 @@ void MachineInfo::send_info_to_debug()
qInfo()<< "";
qInfo()<< " System language defined in QET configuration:"<< QString(QETApp::langFromSetting().toLatin1());
qInfo()<< " language Path:"<< QString(QETApp::languagesPath().toLatin1());
qInfo()<< " Loaded QET translation:"<< QETApp::loadedQetTranslationFile();
qInfo()<< " Loaded Qt translation:"<< QETApp::loadedQtTranslationFile();
qInfo()<< " Common Elements Dir:"<< QString(QETApp::commonElementsDir().toLatin1());
qInfo()<< " Common TitleBlock Templates Dir:"<< QString(QETApp::commonTitleBlockTemplatesDir().toLatin1());
qInfo()<< " Custom Elements Dir:"<< QString(QETApp::customElementsDir().toLatin1());
+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
+30 -3
View File
@@ -197,6 +197,26 @@ QETApp *QETApp::instance()
return m_qetapp;
}
/**
@brief QETApp::loadedQetTranslationFile
@return path of the QET .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQetTranslationFile()
{
return m_qetapp ? m_qetapp->qetTranslator.filePath() : QString();
}
/**
@brief QETApp::loadedQtTranslationFile
@return path of the Qt .qm file actually loaded, empty if none
(diagnostic helper for the startup log, see MachineInfo)
*/
QString QETApp::loadedQtTranslationFile()
{
return m_qetapp ? m_qetapp->qtTranslator.filePath() : QString();
}
/**
@brief QETApp::setLanguage
Change the language used by the application.
@@ -234,14 +254,21 @@ void QETApp::setLanguage(const QString &desired_language) {
// desired_language may be a full locale such as "pt_BR": try that exact
// translation, then the base language ("pt"), then fall back to English.
// French is the application's source language and needs no translation.
// A .qm compiled from an untranslated .ts (0% done) loads "successfully"
// but is empty: treat it as missing, so the user falls back to English
// instead of silently getting the French source strings.
const QString base_language = desired_language.section('_', 0, 0);
bool loaded = qetTranslator.load("qet_" + desired_language, languages_path);
auto loadQet = [this, &languages_path](const QString &name) {
return qetTranslator.load(name, languages_path)
&& !qetTranslator.isEmpty();
};
bool loaded = loadQet("qet_" + desired_language);
if (!loaded && base_language != desired_language)
loaded = qetTranslator.load("qet_" + base_language, languages_path);
loaded = loadQet("qet_" + base_language);
if (!loaded && base_language != "fr") {
// use of the English version by default
// utilisation de la version anglaise par defaut
if(!qetTranslator.load("qet_en", languages_path))
if(!loadQet("qet_en"))
qWarning() << "failed to load"
<< "qet_en" << languages_path << "(" << __FILE__
<< __LINE__ << __FUNCTION__ << ")";
+2
View File
@@ -102,6 +102,8 @@ class QETApp : public QObject
static QString documentDir();
static QString pictureDir();
static QString languagesPath();
static QString loadedQetTranslationFile();
static QString loadedQtTranslationFile();
static QString realPath(const QString &);
static QString symbolicPath(const QString &);
static QStringList handledFileExtensions();
+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>
+9
View File
@@ -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)
+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"