Merge pull request #1318 from ispyisail/feature/bom-skip-junctions-slaves
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m7s

Add options to leave junctions and contact blocks out of the parts list
This commit is contained in:
Laurent Trinques
2026-10-05 17:59:05 +02:00
committed by GitHub
18 changed files with 298 additions and 49 deletions
+12 -4
View File
@@ -42,10 +42,18 @@ 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
The default BOM query exports one row per Simple, Master, Slave or Terminal
element: an auxiliary contact block and a terminal block are parts of their
own (discussion #847). Reports, thumbnails and conductor definitions are
excluded. A Master device is counted once and PLC I/O entries are not
expanded.
Two options leave rows out (#1178). Contact blocks: uncheck **Contacts
esclaves** in the dialog, or pass `--no-slaves`. Junctions, the
terminal-type symbols drawn where wires branch (`114_connections`), which
have no label, designation, manufacturer or manufacturer reference: check
**Laisser de côté les jonctions**, or pass `--no-junctions`. A terminal block
with a label or a part number is kept. The existing `exclude_from_bom` property is honored by the
SQL view, including common true representations (`true`, `1`, `yes`, `on`).
The default columns are:
+25 -10
View File
@@ -1026,7 +1026,17 @@ Note: these options DO NOT allow or block auto numberings, only their update pol
<translation>Format as a part list</translation>
</message>
<message>
<location filename="../sources/ui/bomexportdialog.ui" line="64"/>
<location filename="../sources/ui/bomexportdialog.ui" line="46"/>
<source>Bornes sans repère, désignation, fabricant ni référence fabricant</source>
<translation>Terminals with no label, designation, manufacturer or manufacturer reference</translation>
</message>
<message>
<location filename="../sources/ui/bomexportdialog.ui" line="49"/>
<source>Laisser de côté les jonctions</source>
<translation>Leave out junctions</translation>
</message>
<message>
<location filename="../sources/ui/bomexportdialog.ui" line="77"/>
<source>Aperçu</source>
<translation>Preview</translation>
</message>
@@ -4214,47 +4224,52 @@ Continue?</translation>
<translation>PLCs (MAE/SPS)</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="337"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="324"/>
<source>Contacts esclaves</source>
<translation>Slave contacts</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="347"/>
<source>Configuration</source>
<translation>Configuration</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="346"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="356"/>
<source>Ouvrir la configuration sélectionné</source>
<translation>Open the selected configuration</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="372"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="382"/>
<source>Sauvegarder la configuration actuelle</source>
<translation>Save the current configuration</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="388"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="398"/>
<source>Importer des rapports depuis un fichier</source>
<translation>Import reports from a file</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="391"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="401"/>
<source>Importer...</source>
<translation>Import...</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="398"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="408"/>
<source>Exporter tous les rapports enregistrés vers un fichier</source>
<translation>Export all saved reports to a file</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="401"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="411"/>
<source>Exporter...</source>
<translation>Export...</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="423"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="433"/>
<source>Requête SQL personnalisée</source>
<translation>Custom SQL Query</translation>
</message>
<message>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="433"/>
<location filename="../sources/dataBase/ui/elementquerywidget.ui" line="443"/>
<source>Requête SQL :</source>
<translation>SQL request :</translation>
</message>
+6
View File
@@ -703,6 +703,12 @@ Python, plus the hang guard on `addConductor` and the database refresh in
`source_date_epoch` if given, else the server's own `SOURCE_DATE_EPOCH`,
else 0 (1 January 1970). The result's `"reproducible"` is false, with a
hint, when the QElectroTech build is too old to honour it.
- **`"no_slaves"` and `"no_junctions"` shorten a `bom` export.** The first
leaves out the contact blocks, which otherwise get a row of their own; the
second leaves out terminal-type elements with no label, designation,
manufacturer or manufacturer reference, such as the junction dots of
`114_connections`. They pass `--no-slaves` and `--no-junctions`; an older
QElectroTech ignores both and exports every row, so check the row count.
- **`qet_export` isolates its launch.** SingleApplication keys its socket
on `applicationFilePath()`, so a second launch of the same binary path
forwards its request to an already-running instance and returns *that*
+22 -4
View File
@@ -1061,10 +1061,13 @@ def _source_date_epoch(value) -> int:
def tool_export(binary: str, project: str, format: str, output: str,
timeout: int = 180, reproducible: bool = False,
source_date_epoch: int | None = None) -> dict:
source_date_epoch: int | None = None,
no_slaves: bool = False, no_junctions: bool = False) -> dict:
if format not in EXPORT_FORMATS:
raise ValueError(f"unknown format {format!r}; "
f"expected one of {', '.join(sorted(EXPORT_FORMATS))}")
if (no_slaves or no_junctions) and format != "bom":
raise ValueError("no_slaves and no_junctions only apply to format \"bom\"")
proj = Path(project).expanduser()
if not proj.is_file():
raise ValueError(f"no such project: {proj}")
@@ -1082,8 +1085,12 @@ def tool_export(binary: str, project: str, format: str, output: str,
if reproducible or source_date_epoch is not None:
epoch = _source_date_epoch(source_date_epoch)
extra_env = {"SOURCE_DATE_EPOCH": str(epoch)}
result = _run_qet(binary, [flag, str(proj), output], timeout,
extra_env=extra_env)
args = [flag, str(proj), output]
if no_slaves:
args.append("--no-slaves")
if no_junctions:
args.append("--no-junctions")
result = _run_qet(binary, args, timeout, extra_env=extra_env)
out = Path(output).expanduser()
result["output"] = str(out)
result["output_exists"] = out.exists()
@@ -4822,13 +4829,24 @@ TOOLS = [
"carries, in seconds since 1970 UTC. "
"Default: this server's own "
"SOURCE_DATE_EPOCH, else 0"},
"no_slaves": {"type": "boolean", "default": False,
"description": "bom: leave out the contact blocks (slave "
"elements), which otherwise get a row of "
"their own with their master's label"},
"no_junctions": {"type": "boolean", "default": False,
"description": "bom: leave out the junctions: "
"terminal-type elements with no label, "
"designation, manufacturer or "
"manufacturer reference"},
},
"required": ["project", "format", "output"],
},
"handler": lambda a: tool_export(a["binary"], a["project"], a["format"],
a["output"], a.get("timeout", 180),
a.get("reproducible", False),
a.get("source_date_epoch")),
a.get("source_date_epoch"),
a.get("no_slaves", False),
a.get("no_junctions", False)),
},
{
"name": "qet_edit",
+30
View File
@@ -4034,6 +4034,36 @@ class ProjectNewValidation(unittest.TestCase):
self.assertFalse(Path(new).exists())
class BomExportOptions(unittest.TestCase):
"""qet_export's no_slaves and no_junctions become --export-bom flags (#1178)."""
def run_export(self, format="bom", **kw):
seen = []
def fake(binary, args, timeout=180, **rest):
seen.extend(args)
return {"ok": True}
with tempfile.TemporaryDirectory() as tmp:
proj = Path(tmp) / "a.qet"
proj.write_text("<project/>")
with unittest.mock.patch.object(m, "_run_qet", fake):
m.tool_export("qet", str(proj), format, str(Path(tmp) / "o.csv"), **kw)
return seen
def test_off_by_default(self):
args = self.run_export()
self.assertNotIn("--no-slaves", args)
self.assertNotIn("--no-junctions", args)
def test_flags_after_the_output(self):
args = self.run_export(no_slaves=True, no_junctions=True)
self.assertEqual(args[0], "--export-bom")
self.assertEqual(args[3:], ["--no-slaves", "--no-junctions"])
def test_refused_for_other_formats(self):
with self.assertRaises(ValueError):
self.run_export(format="wiring", no_junctions=True)
class ReproducibleExport(unittest.TestCase):
"""qet_export's "reproducible" sets SOURCE_DATE_EPOCH for the run."""
+34 -4
View File
@@ -57,7 +57,32 @@ QStringList BomExport::defaultColumns()
};
}
QString BomExport::defaultQuery()
/**
@brief BomExport::junctionFilter
@return a condition true for every row that is not a junction: a
terminal-type element with no label and no part data. Some users draw
the dots and bends where wires branch as their own terminal-type
symbols, and each one became an empty row of the parts list (#1178).
A terminal block with a label or a part number is kept.
*/
QString BomExport::junctionFilter()
{
return QStringLiteral("NOT (element_type = 'terminal' "
"AND IFNULL(label, '') = '' "
"AND IFNULL(designation, '') = '' "
"AND IFNULL(manufacturer, '') = '' "
"AND IFNULL(manufacturer_reference, '') = '')");
}
/**
@brief BomExport::defaultQuery
@param include_slaves : false leaves out the contact blocks (slave
elements), which otherwise appear as rows of their own
@param include_junctions : false leaves out the junctions, see
junctionFilter()
@return the query of the parts list
*/
QString BomExport::defaultQuery(bool include_slaves, bool include_junctions)
{
//Slaves and terminals are included because both are routinely
//separately orderable hardware. A circuit breaker can carry ten or
@@ -77,11 +102,16 @@ QString BomExport::defaultQuery()
//and the conductor definition stay out because they are not hardware.
//
//See discussion #847.
const QString types = include_slaves
? QStringLiteral("'simple', 'master', 'slave', 'terminal'")
: QStringLiteral("'simple', 'master', 'terminal'");
const QString junctions = include_junctions
? QString()
: QStringLiteral(" AND ") + junctionFilter();
return QStringLiteral("SELECT %1 FROM element_nomenclature_view "
"WHERE element_type IN "
"('simple', 'master', 'slave', 'terminal') "
"WHERE element_type IN (%2)%3 "
"ORDER BY diagram_position, position, label")
.arg(defaultColumns().join(QStringLiteral(", ")));
.arg(defaultColumns().join(QStringLiteral(", ")), types, junctions);
}
QByteArray BomExport::toCsv(QSqlQuery &query, const QStringList &headers,
+3 -1
View File
@@ -27,7 +27,9 @@ class QSqlQuery;
namespace BomExport
{
QStringList defaultColumns();
QString defaultQuery();
QString defaultQuery(bool include_slaves = true,
bool include_junctions = true);
QString junctionFilter();
QByteArray toCsv(QSqlQuery &query, const QStringList &headers,
bool include_headers = true, int *row_count = nullptr);
bool writeCsv(const QString &file_path, const QByteArray &csv,
+8 -3
View File
@@ -408,10 +408,12 @@ int exportCsv(QETProject &project, const QString &format, const QString &output)
/// Bill of materials from the same project database and default query as the
/// GUI nomenclature export.
int exportBom(QETProject &project, const QString &output)
int exportBom(QETProject &project, const QString &output,
bool includeSlaves, bool includeJunctions)
{
project.dataBase()->updateDB();
QSqlQuery query = project.dataBase()->newQuery(BomExport::defaultQuery());
QSqlQuery query = project.dataBase()->newQuery(
BomExport::defaultQuery(includeSlaves, includeJunctions));
if (!query.exec()) {
err << "BOM query failed: " << query.lastError().text() << "\n";
return 1;
@@ -980,6 +982,9 @@ int run(const QStringList &args)
// collected below.
QStringList filtered = args;
const bool showTerminals = filtered.removeAll("--show-terminals") > 0;
// --no-slaves and --no-junctions leave rows out of --export-bom.
const bool includeSlaves = filtered.removeAll("--no-slaves") == 0;
const bool includeJunctions = filtered.removeAll("--no-junctions") == 0;
QString flag;
QStringList rest;
@@ -1037,7 +1042,7 @@ int run(const QStringList &args)
if (format == "cables" || format == "wires")
return exportCsv(project, format, output);
if (format == "bom")
return exportBom(project, output);
return exportBom(project, output, includeSlaves, includeJunctions);
if (format == "wiring")
return exportWiring(project, output);
if (format == "nets")
+6 -1
View File
@@ -48,7 +48,7 @@ namespace CLIExport {
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
qelectrotech --export-cables <project.qet> <output.csv>
qelectrotech --export-wires <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv>
qelectrotech --export-bom <project.qet> <output.csv> [--no-slaves] [--no-junctions]
qelectrotech --export-wiring <project.qet> <output.csv>
qelectrotech --export-nets <project.qet> <output.json>
qelectrotech --export-links <project.qet> <output.csv>
@@ -69,6 +69,11 @@ namespace CLIExport {
cables: wiring list (one row per conductor) as CSV.
wires: list of distinct wire numbers as CSV.
bom: bill of materials (one row per element) as CSV.
--no-slaves: leave the contact blocks (slave elements) out of the
bill of materials.
--no-junctions: leave the junctions out of the bill of materials:
terminal-type elements with no label, designation,
manufacturer or manufacturer reference.
wiring: from-to wiring list (one row per conductor) as CSV, read from
the project database. Same logical list as `cables`, which
reads the document XML instead; the two are meant to agree,
+75 -18
View File
@@ -56,6 +56,7 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
m_button_group.addButton(ui->m_protection_cb, 5);
m_button_group.addButton(ui->m_thumbnail_cb, 6);
m_button_group.addButton(ui->m_plc_cb, 7);
m_button_group.addButton(ui->m_slave_cb, 8);
connect(&m_button_group, static_cast<void (QButtonGroup::*)(int)>(&QButtonGroup::idClicked), [this](int id)
{
@@ -78,27 +79,12 @@ ElementQueryWidget::ElementQueryWidget(QWidget *parent) :
}
else
{
int checked = 0;
for (int i=1 ; i<8 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
switch (checked)
{
case 0 :
check_box->setCheckState(Qt::Unchecked);
break;
case 7:
check_box->setCheckState(Qt::Checked);
break;
default:
check_box->setCheckState(Qt::PartiallyChecked);
break;
}
updateAllCheckState();
}
updateQueryLine();
});
updateAllCheckState();
setUpItems();
fillSavedQuery();
@@ -203,8 +189,12 @@ void ElementQueryWidget::setQuery(const QString &query)
if (ui->m_plc_cb->isChecked()) {
++c;
}
ui->m_slave_cb->setChecked (str_type.contains(ElementData::typeToString(ElementData::Slave)));
if (ui->m_slave_cb->isChecked()) {
++c;
}
if (c == 7) {
if (c == 8) {
ui->m_all_cb->setCheckState(Qt::Checked);
} else if (c > 0) {
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
@@ -395,6 +385,11 @@ QString ElementQueryWidget::queryStr() const
where += QStringLiteral(" element_sub_type = '") += ElementData::masterTypeToString(ElementData::PLC) += "'";
b = true;
}
if (ui->m_slave_cb->isChecked()) {
if (b) where +=" OR";
where += QStringLiteral(" element_type = '") += ElementData::typeToString(ElementData::Slave) += "'";
b = true;
}
where.append(")");
if (where == " WHERE ()") {
@@ -406,6 +401,10 @@ QString ElementQueryWidget::queryStr() const
// widget's query reads FROM that view, so a flagged element never
// reaches this point in the first place.
if (!m_extra_filter.isEmpty()) {
filter_ += QStringLiteral(" AND ") + m_extra_filter;
}
if (where.isEmpty() && !filter_.isEmpty()) {
filter_.remove(0, 4); //Remove the first " AND" of filter.
filter_.prepend( " WHERE");
@@ -456,6 +455,64 @@ void ElementQueryWidget::setCount(QString text, bool set)
updateQueryLine();
}
/**
@brief ElementQueryWidget::setSlavesIncluded
Check or uncheck the contact blocks (slave elements) in the element
type filter. The box is unchecked by default, which keeps the tables
made before it existed as they were.
@param included
*/
void ElementQueryWidget::setSlavesIncluded(bool included)
{
ui->m_slave_cb->setChecked(included);
updateAllCheckState();
updateQueryLine();
}
/**
@brief ElementQueryWidget::updateAllCheckState
Set the "all" check box from the element type check boxes.
*/
void ElementQueryWidget::updateAllCheckState()
{
int checked = 0;
for (int i=1 ; i<9 ; ++i) {
if (m_button_group.button(i)->isChecked()) {++checked;}
}
switch (checked)
{
case 0 :
ui->m_all_cb->setCheckState(Qt::Unchecked);
break;
case 8:
ui->m_all_cb->setCheckState(Qt::Checked);
break;
default:
ui->m_all_cb->setCheckState(Qt::PartiallyChecked);
break;
}
}
/**
@brief ElementQueryWidget::setExtraFilter
Add a condition every row of the query must meet, on top of the filters
chosen in this widget.
@param text : the condition, ex : "label != ''"
@param set :
true by default -> the condition is used.
false -> the condition is removed.
*/
void ElementQueryWidget::setExtraFilter(const QString &text, bool set)
{
if (set) {
m_extra_filter = text;
} else {
m_extra_filter.clear();
}
updateQueryLine();
}
/**
@brief ElementQueryWidget::updateQueryLine
*/
+5 -1
View File
@@ -45,6 +45,8 @@ class ElementQueryWidget : public QWidget
QString queryStr() const;
void setGroupBy(QString text, bool set = true);
void setCount(QString text, bool set = true);
void setSlavesIncluded(bool included);
void setExtraFilter(const QString &text, bool set = true);
static QString modelIdentifier() {return "nomenclature";}
@@ -67,6 +69,7 @@ class ElementQueryWidget : public QWidget
void on_m_choosen_list_itemDoubleClicked(QListWidgetItem *item);
void updateQueryLine();
void updateAllCheckState();
QStringList selectedKeys() const;
void setUpItems();
QPair<int, QString> FilterFor(const QString &key) const;
@@ -82,7 +85,8 @@ class ElementQueryWidget : public QWidget
QList <QListWidgetItem *> m_items_list;
QString m_custom_query,
m_group_by,
m_count;
m_count,
m_extra_filter;
QHash <QString, QPair<int, QString>> m_filter;
};
+10
View File
@@ -318,6 +318,16 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="m_slave_cb">
<property name="text">
<string>Contacts esclaves</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
+6 -2
View File
@@ -325,9 +325,13 @@ bool QetScriptApi::exportWires(const QString &output)
return runFlag(QStringLiteral("--export-wires"), {output});
}
bool QetScriptApi::exportBom(const QString &output)
bool QetScriptApi::exportBom(const QString &output, bool noSlaves,
bool noJunctions)
{
return runFlag(QStringLiteral("--export-bom"), {output});
QStringList args{output};
if (noSlaves) args << QStringLiteral("--no-slaves");
if (noJunctions) args << QStringLiteral("--no-junctions");
return runFlag(QStringLiteral("--export-bom"), args);
}
bool QetScriptApi::exportWiring(const QString &output)
+2 -1
View File
@@ -384,7 +384,8 @@ class QetScriptApi : public QObject
Q_INVOKABLE bool exportDxf(const QString &outDir, bool showTerminals = false);
Q_INVOKABLE bool exportCables(const QString &output);
Q_INVOKABLE bool exportWires(const QString &output);
Q_INVOKABLE bool exportBom(const QString &output);
Q_INVOKABLE bool exportBom(const QString &output, bool noSlaves = false,
bool noJunctions = false);
Q_INVOKABLE bool exportWiring(const QString &output);
Q_INVOKABLE bool exportNets(const QString &output);
Q_INVOKABLE bool exportLinks(const QString &output);
+12
View File
@@ -46,6 +46,9 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_query_widget = new ElementQueryWidget(this);
ui->m_main_layout->insertWidget(0, m_query_widget);
m_query_widget->setQuery(BomExport::defaultQuery());
//The contact blocks are parts of their own, as in the command line
//export; unchecking "Contacts esclaves" leaves them out.
m_query_widget->setSlavesIncluded(true);
on_m_format_as_bom_clicked(false);
m_preview_model = new QSqlQueryModel(this);
@@ -145,6 +148,15 @@ void BOMExportDialog::on_m_format_as_bom_clicked(bool checked) {
m_query_widget->setCount("COUNT(*) AS designation_qty", checked);
}
/**
@brief BOMExportDialog::on_m_no_junctions_clicked
@param checked : true leaves out the junctions, see
BomExport::junctionFilter()
*/
void BOMExportDialog::on_m_no_junctions_clicked(bool checked) {
m_query_widget->setExtraFilter(BomExport::junctionFilter(), checked);
}
/**
@brief BOMExportDialog::on_m_preview_pb_clicked
Run the current query and show its result live, without going through
+1
View File
@@ -45,6 +45,7 @@ class BOMExportDialog : public QDialog
private slots:
void on_m_format_as_bom_clicked(bool checked);
void on_m_no_junctions_clicked(bool checked);
void on_m_preview_pb_clicked();
private:
+13
View File
@@ -40,6 +40,19 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_no_junctions">
<property name="toolTip">
<string>Bornes sans repère, désignation, fabricant ni référence fabricant</string>
</property>
<property name="text">
<string>Laisser de côté les jonctions</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
+28
View File
@@ -120,6 +120,34 @@ void SmartDeviceTest::defaultQueryAndCsv()
QVERIFY(csv.contains("Terminal block"));
QVERIFY(!csv.contains("Must not be exported"));
QVERIFY(csv.indexOf("-K1") < csv.indexOf("-K2"));
//#1178: a junction -- a terminal-type symbol drawn where wires
//branch -- has no label and no part data. It can be left out, and
//so can the contact blocks; a terminal block with only a part
//number stays.
QSqlQuery raw(db);
QVERIFY(raw.exec(QStringLiteral(
"INSERT INTO element_nomenclature_view "
"(label, element_type, diagram_position, position) "
"VALUES (NULL, 'terminal', 1, 6)")));
QVERIFY(raw.exec(QStringLiteral(
"INSERT INTO element_nomenclature_view "
"(label, designation, manufacturer, manufacturer_reference, "
"element_type, diagram_position, position) "
"VALUES ('', '', '', 'PT 2,5', 'terminal', 1, 7)")));
auto count = [&db](const QString &sql) {
QSqlQuery q(db);
if (!q.exec(sql))
return -1;
int n = 0;
while (q.next())
++n;
return n;
};
QCOMPARE(count(BomExport::defaultQuery()), 6);
QCOMPARE(count(BomExport::defaultQuery(true, false)), 5);
QCOMPARE(count(BomExport::defaultQuery(false, true)), 5);
QCOMPARE(count(BomExport::defaultQuery(false, false)), 4);
}
QSqlDatabase::removeDatabase(connection);
}