mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-30 23:04:13 +02:00
d5ae15f568
The element description dialog only offered typing, so every article of a catalogue had to be copied in by hand into the Artikelbeschreibung row of each block, over and over. Settings > General now keeps the path of a CSV material list (Créer... writes a template with the header lines), and the Artikelbeschreibung row of every block of the Bauteil dialog shows a "..." button opening a searchable listing of that file. "Appliquer" fills the fields of that block only -- the main block gets everything but label and formula, an auxiliary block gets description, number, manufacturer, references, supplier, quantity, unit and additional info -- and a cell that is empty in the file never erases a value already in the element. The symbol editor is left alone. - sources/materiallist/: CSV reading and writing (UTF-8 BOM, ';', every field quoted, atomic write through QSaveFile), two header lines (translated labels then the canonical English keys), the selection window and the new entry form. - The selection window reads the file again every time it opens, keeps its size, column widths, column order and sort between openings, shows every column of the file, scrolls all four directions with the wheel (Shift + wheel moves the columns) and puts back the order of the file when the corner above the row numbers or a row number is clicked. - German translations for the new strings in lang/qet_de.ts.
779 lines
21 KiB
C++
779 lines
21 KiB
C++
/*
|
|
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 "materiallist.h"
|
|
|
|
#include "../qetinformation.h"
|
|
|
|
#include <QCoreApplication>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QSaveFile>
|
|
#include <QSettings>
|
|
#include <QStandardPaths>
|
|
|
|
#include <utility>
|
|
|
|
namespace {
|
|
|
|
/**
|
|
@brief Decode a csv file content.
|
|
The material file is written in utf-8 with a BOM, the same way the bill
|
|
of material export does, so spreadsheets open it with the right
|
|
encoding. A file written by hand in the local encoding is still
|
|
decodable: fromUtf8() is only used when the bytes are valid utf-8.
|
|
@param raw
|
|
@return
|
|
*/
|
|
QString decode(const QByteArray &raw)
|
|
{
|
|
if (raw.startsWith("\xEF\xBB\xBF")) {
|
|
return QString::fromUtf8(raw.constData() + 3, raw.size() - 3);
|
|
}
|
|
|
|
const QByteArray utf8 = QString::fromUtf8(raw).toUtf8();
|
|
if (utf8 == raw) {
|
|
return QString::fromUtf8(raw);
|
|
}
|
|
return QString::fromLocal8Bit(raw);
|
|
}
|
|
|
|
/**
|
|
@brief Tell whether a file exists and holds something.
|
|
An existing but empty file is the same as no file at all: it can be
|
|
filled with the template without losing anything.
|
|
@param path
|
|
@return
|
|
*/
|
|
bool hasContent(const QString &path)
|
|
{
|
|
const QFileInfo info(path);
|
|
return info.exists() && info.isFile() && info.size() > 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/**
|
|
@brief MaterialList::settingsKey
|
|
@return the QSettings key holding the path of the material file
|
|
*/
|
|
QString MaterialList::settingsKey()
|
|
{
|
|
return QStringLiteral("elements-collections/material-list-path");
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::configuredPath
|
|
@return the path of the material file as chosen in the preferences,
|
|
or a null QString when the user never set one.
|
|
*/
|
|
QString MaterialList::configuredPath()
|
|
{
|
|
QSettings settings;
|
|
const QString path = settings.value(settingsKey(), QStringLiteral("default")).toString();
|
|
if (path.isEmpty() || path == QLatin1String("default")) {
|
|
return QString();
|
|
}
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::setConfiguredPath
|
|
Store the path of the material file in the preferences.
|
|
@param path
|
|
*/
|
|
void MaterialList::setConfiguredPath(const QString &path)
|
|
{
|
|
QSettings settings;
|
|
settings.setValue(settingsKey(), path.isEmpty() ? QStringLiteral("default") : path);
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::defaultFileName
|
|
@return the file name used when the material file is created without an
|
|
explicit location.
|
|
*/
|
|
QString MaterialList::defaultFileName()
|
|
{
|
|
return QStringLiteral("qet_material_list.csv");
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::defaultPath
|
|
@return where the material file is created when no path is configured
|
|
yet : the standard documents folder.
|
|
*/
|
|
QString MaterialList::defaultPath()
|
|
{
|
|
QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
|
while (dir.endsWith(QLatin1Char('/'))) {
|
|
dir.chop(1);
|
|
}
|
|
if (dir.isEmpty()) {
|
|
dir = QDir::currentPath();
|
|
}
|
|
return dir + QLatin1Char('/') + defaultFileName();
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::savedHeaderState
|
|
@return the saved column layout of the selection window, empty when the
|
|
window was never opened or the layout could not be read back.
|
|
*/
|
|
QByteArray MaterialList::savedHeaderState()
|
|
{
|
|
QSettings settings;
|
|
return settings.value(QStringLiteral("elements-collections/material-list-header")).toByteArray();
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::saveHeaderState
|
|
Remember the column layout of the selection window.
|
|
@param state the header state as saved by QHeaderView::saveState()
|
|
*/
|
|
void MaterialList::saveHeaderState(const QByteArray &state)
|
|
{
|
|
QSettings settings;
|
|
settings.setValue(QStringLiteral("elements-collections/material-list-header"), state);
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::savedDialogSize
|
|
@return the size the selection window had the last time it was closed,
|
|
an invalid size when it was never opened.
|
|
*/
|
|
QSize MaterialList::savedDialogSize()
|
|
{
|
|
QSettings settings;
|
|
const QSize size(settings.value(QStringLiteral("elements-collections/material-list-width")).toInt(),
|
|
settings.value(QStringLiteral("elements-collections/material-list-height")).toInt());
|
|
//A size which was never written reads back as 0x0, which Qt still
|
|
//counts as valid : it must not be handed to resize().
|
|
return size.isEmpty() ? QSize() : size;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::saveDialogSize
|
|
Remember the size of the selection window.
|
|
@param size
|
|
*/
|
|
void MaterialList::saveDialogSize(const QSize &size)
|
|
{
|
|
if (size.isEmpty()) {
|
|
return;
|
|
}
|
|
QSettings settings;
|
|
settings.setValue(QStringLiteral("elements-collections/material-list-width"), size.width());
|
|
settings.setValue(QStringLiteral("elements-collections/material-list-height"), size.height());
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::defaultColumns
|
|
@return the columns of a brand new material file, in writing order.
|
|
It is every information of the main article (everything but label and
|
|
formula, which QElectroTech computes itself) plus the one column that
|
|
describes an auxiliary block.
|
|
*/
|
|
QStringList MaterialList::defaultColumns()
|
|
{
|
|
QStringList columns = columnsForBlock(0);
|
|
columns << QStringLiteral("auxiliary");
|
|
return columns;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::columnsForBlock
|
|
Columns of the material file that can be transferred to a block of the
|
|
element informations.
|
|
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
|
@return
|
|
*/
|
|
QStringList MaterialList::columnsForBlock(int block)
|
|
{
|
|
static const QStringList main_columns = {
|
|
QStringLiteral("description"),
|
|
QStringLiteral("designation"),
|
|
QStringLiteral("manufacturer"),
|
|
QStringLiteral("manufacturer_reference"),
|
|
QStringLiteral("machine_manufacturer_reference"),
|
|
QStringLiteral("supplier"),
|
|
QStringLiteral("quantity"),
|
|
QStringLiteral("unity"),
|
|
QStringLiteral("function"),
|
|
QStringLiteral("comment"),
|
|
QStringLiteral("notes"),
|
|
QStringLiteral("model"),
|
|
QStringLiteral("category"),
|
|
QStringLiteral("voltage_rating"),
|
|
QStringLiteral("current_rating"),
|
|
QStringLiteral("plant"),
|
|
QStringLiteral("location"),
|
|
QStringLiteral("width"),
|
|
QStringLiteral("height"),
|
|
QStringLiteral("depth")
|
|
};
|
|
|
|
static const QStringList auxiliary_columns = {
|
|
QStringLiteral("description"),
|
|
QStringLiteral("designation"),
|
|
QStringLiteral("manufacturer"),
|
|
QStringLiteral("manufacturer_reference"),
|
|
QStringLiteral("machine_manufacturer_reference"),
|
|
QStringLiteral("supplier"),
|
|
QStringLiteral("quantity"),
|
|
QStringLiteral("unity"),
|
|
QStringLiteral("auxiliary")
|
|
};
|
|
|
|
if (block <= 0) {
|
|
return main_columns;
|
|
}
|
|
if (block > 4) {
|
|
return QStringList();
|
|
}
|
|
return auxiliary_columns;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::elementInfoKey
|
|
Tell which element information a column of the material file feeds
|
|
when it is applied to a block.
|
|
@param column a column of the material file
|
|
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
|
@return the element information key, or a null QString when that column
|
|
does not belong to that block
|
|
*/
|
|
QString MaterialList::elementInfoKey(const QString &column, int block)
|
|
{
|
|
if (!columnsForBlock(block).contains(column)) {
|
|
return QString();
|
|
}
|
|
|
|
if (block <= 0) {
|
|
return column;
|
|
}
|
|
|
|
//Every auxiliary column carries the same name as its main counterpart,
|
|
//only the suffix tells the block apart. The block description column
|
|
//is the exception: it is named auxiliary in the file and auxiliary1 to
|
|
//auxiliary4 in the element informations.
|
|
if (column == QLatin1String("auxiliary")) {
|
|
return QStringLiteral("auxiliary%1").arg(block);
|
|
}
|
|
return column + QStringLiteral("_auxiliary%1").arg(block);
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::translatedColumn
|
|
@param column a column of the material file
|
|
@return that column named the way it is shown to the user, in the
|
|
current language. A column QElectroTech doesn't know is returned as it
|
|
is, since it can only have been named by the user.
|
|
*/
|
|
QString MaterialList::translatedColumn(const QString &column)
|
|
{
|
|
if (column == QLatin1String("auxiliary")) {
|
|
return QCoreApplication::translate("MaterialList", "Bloc auxiliaire");
|
|
}
|
|
|
|
const QString translated = QETInformation::translatedInfoKey(column);
|
|
return translated.isEmpty() ? column : translated;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::translatedHeader
|
|
The label line of a material file : the columns the way the user reads
|
|
them, in the current language.
|
|
@param columns
|
|
@return
|
|
*/
|
|
QStringList MaterialList::translatedHeader(const QStringList &columns)
|
|
{
|
|
QStringList header;
|
|
header.reserve(columns.size());
|
|
for (const QString &column : columns) {
|
|
header.append(translatedColumn(column));
|
|
}
|
|
return header;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::headerAliasMap
|
|
@return every accepted header cell of a material file, lowercased,
|
|
mapped to the canonical column it means.
|
|
Beside the key itself, a header may use the translated name of the
|
|
information ("Artikelbeschreibung" for description), so a file written
|
|
by hand in the user's language is understood as well as the canonical
|
|
one.
|
|
*/
|
|
QMap<QString, QString> MaterialList::headerAliasMap()
|
|
{
|
|
QMap<QString, QString> map;
|
|
const QStringList keys = QETInformation::elementInfoKeys();
|
|
|
|
for (const QString &key : keys)
|
|
{
|
|
map.insert(key.toLower(), key);
|
|
const QString translated = QETInformation::translatedInfoKey(key);
|
|
if (!translated.isEmpty()) {
|
|
map.insert(translated.toLower(), key);
|
|
}
|
|
}
|
|
|
|
map.insert(QStringLiteral("auxiliary"), QStringLiteral("auxiliary"));
|
|
const QString auxiliary_label = QCoreApplication::translate("MaterialList", "Bloc auxiliaire").toLower();
|
|
map.insert(auxiliary_label, QStringLiteral("auxiliary"));
|
|
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::canonicalColumn
|
|
Turn one header cell into a canonical column name.
|
|
@param header_cell
|
|
@param already_used the names chosen for the previous cells, to keep
|
|
the header free of duplicates
|
|
@param alias_map lowercased alias -> canonical name
|
|
@return
|
|
*/
|
|
QString MaterialList::canonicalColumn(const QString &header_cell,
|
|
const QStringList &already_used,
|
|
const QMap<QString, QString> &alias_map)
|
|
{
|
|
const QString raw = header_cell.trimmed();
|
|
|
|
if (raw.isEmpty()) {
|
|
//A nameless column keeps its data and its place, it just can't be
|
|
//matched to an element information.
|
|
QString name = QStringLiteral("column_%1").arg(already_used.size() + 1);
|
|
int i = already_used.size() + 1;
|
|
while (already_used.contains(name)) {
|
|
name = QStringLiteral("column_%1").arg(++i);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
const auto it = alias_map.constFind(raw.toLower());
|
|
if (it != alias_map.constEnd() && !already_used.contains(it.value())) {
|
|
return it.value();
|
|
}
|
|
|
|
//Either an unknown name, or a second cell meaning something which is
|
|
//already used : the cell is kept as it is written, its data is never
|
|
//thrown away.
|
|
QString name = raw;
|
|
int i = 2;
|
|
while (already_used.contains(name)) {
|
|
name = raw + QStringLiteral("_%1").arg(i++);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::isMachineHeaderLine
|
|
Tell whether the second line of a material file is the machine header
|
|
QElectroTech writes under the label line : the canonical column names,
|
|
kept so that a file written in one language is still understood in
|
|
another one.
|
|
|
|
The check is deliberately strict about what it recognises and lenient
|
|
about the rest : every cell is either a known column name, or the very
|
|
same text as the label above it (which is how an unknown column shows
|
|
up in both lines). A data row holding free text never passes.
|
|
@param label_line the first line of the file, the one the user reads
|
|
@param candidate the line which may be the machine header
|
|
@param alias_map lowercased alias -> canonical name
|
|
@return true when candidate must not be read as a record
|
|
*/
|
|
bool MaterialList::isMachineHeaderLine(const QStringList &label_line,
|
|
const QStringList &candidate,
|
|
const QMap<QString, QString> &alias_map)
|
|
{
|
|
if (candidate.isEmpty() || candidate.size() != label_line.size()) {
|
|
return false;
|
|
}
|
|
|
|
bool has_known_column = false;
|
|
for (int i = 0; i < candidate.size(); ++i)
|
|
{
|
|
const QString cell = candidate.at(i).trimmed();
|
|
if (cell.isEmpty()) {
|
|
continue;
|
|
}
|
|
if (alias_map.contains(cell.toLower()))
|
|
{
|
|
has_known_column = true;
|
|
continue;
|
|
}
|
|
if (cell == label_line.at(i).trimmed()) {
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return has_known_column;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::detectSeparator
|
|
Tell which separator a csv text uses. Spreadsheets of the german
|
|
region write a semicolon, the english ones a comma, some tools a tab.
|
|
@param first_line the header line
|
|
@return
|
|
*/
|
|
QChar MaterialList::detectSeparator(const QString &first_line)
|
|
{
|
|
const int semicolons = first_line.count(QLatin1Char(';'));
|
|
const int tabs = first_line.count(QLatin1Char('\t'));
|
|
const int commas = first_line.count(QLatin1Char(','));
|
|
|
|
if (semicolons == 0 && tabs == 0 && commas == 0) {
|
|
return QLatin1Char(';');
|
|
}
|
|
if (tabs >= semicolons && tabs >= commas && tabs > 0) {
|
|
return QLatin1Char('\t');
|
|
}
|
|
if (commas > semicolons) {
|
|
return QLatin1Char(',');
|
|
}
|
|
return QLatin1Char(';');
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::parseCsv
|
|
Split a csv text into rows. Quoted fields may contain the separator
|
|
and line breaks, doubled quotes mean one quote.
|
|
@param content
|
|
@param separator
|
|
@return
|
|
*/
|
|
QList<QStringList> MaterialList::parseCsv(const QString &content, QChar separator)
|
|
{
|
|
QList<QStringList> rows;
|
|
QStringList row;
|
|
QString field;
|
|
bool quoted = false;
|
|
|
|
for (int i = 0; i < content.size(); ++i)
|
|
{
|
|
const QChar c = content.at(i);
|
|
|
|
if (quoted)
|
|
{
|
|
if (c == QLatin1Char('"'))
|
|
{
|
|
if (i + 1 < content.size() && content.at(i + 1) == QLatin1Char('"')) {
|
|
field += QLatin1Char('"');
|
|
++i;
|
|
} else {
|
|
quoted = false;
|
|
}
|
|
} else {
|
|
field += c;
|
|
}
|
|
}
|
|
else if (c == QLatin1Char('"') && field.isEmpty())
|
|
{
|
|
quoted = true;
|
|
}
|
|
else if (c == separator)
|
|
{
|
|
row.append(field);
|
|
field.clear();
|
|
}
|
|
else if (c == QLatin1Char('\n'))
|
|
{
|
|
row.append(field);
|
|
field.clear();
|
|
rows.append(row);
|
|
row.clear();
|
|
}
|
|
else if (c != QLatin1Char('\r'))
|
|
{
|
|
field += c;
|
|
}
|
|
}
|
|
|
|
if (!field.isEmpty() || !row.isEmpty()) {
|
|
row.append(field);
|
|
rows.append(row);
|
|
}
|
|
|
|
//A trailing line break produces a last, empty row : without this it
|
|
//would show up as a blank line in the selection dialog.
|
|
while (!rows.isEmpty() && rows.last().size() == 1 && rows.last().first().isEmpty()) {
|
|
rows.removeLast();
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::serializeCsv
|
|
Build the content of a csv file : utf-8 BOM, quoted fields, one line
|
|
per record. Same dialect as the bill of material export.
|
|
The file opens with two header lines : the columns the way the user
|
|
reads them, then the canonical names QElectroTech matches against the
|
|
element informations.
|
|
@param header_lines the header lines, in writing order
|
|
@param rows
|
|
@param separator
|
|
@return
|
|
*/
|
|
QByteArray MaterialList::serializeCsv(const QList<QStringList> &header_lines, const QList<QStringList> &rows, QChar separator)
|
|
{
|
|
//Helpers duplicated from the bill of material export : every field is
|
|
//quoted, so a semicolon inside a value is never read as a separator.
|
|
auto escape = [](QString value) {
|
|
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
|
|
return QLatin1Char('"') + value + QLatin1Char('"');
|
|
};
|
|
|
|
auto line = [&](const QStringList &values) {
|
|
QStringList escaped;
|
|
escaped.reserve(values.size());
|
|
for (const QString &value : values) {
|
|
escaped.append(escape(value));
|
|
}
|
|
return escaped.join(separator) + QLatin1Char('\n');
|
|
};
|
|
|
|
QByteArray out("\xEF\xBB\xBF");
|
|
for (const QStringList &header_line : header_lines) {
|
|
out += line(header_line).toUtf8();
|
|
}
|
|
for (const QStringList &row : rows) {
|
|
out += line(row).toUtf8();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::load
|
|
Read a material file.
|
|
@param path
|
|
@param data receives the columns and the records
|
|
@param error receives a message when the file can't be read
|
|
@return true on success
|
|
*/
|
|
bool MaterialList::load(const QString &path, MaterialListData *data, QString *error)
|
|
{
|
|
if (error) {
|
|
error->clear();
|
|
}
|
|
if (!data) {
|
|
if (error) {
|
|
*error = QCoreApplication::translate("MaterialList", "Aucun récepteur pour le répertoire de matériaux.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
data->columns.clear();
|
|
data->records.clear();
|
|
data->separator = QLatin1Char(';');
|
|
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
{
|
|
if (error) {
|
|
*error = file.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
const QByteArray raw = file.readAll();
|
|
file.close();
|
|
|
|
const QString content = decode(raw);
|
|
if (content.trimmed().isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
const int eol = content.indexOf(QLatin1Char('\n'));
|
|
data->separator = detectSeparator(eol < 0 ? content : content.left(eol));
|
|
|
|
QList<QStringList> rows = parseCsv(content, data->separator);
|
|
if (rows.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
const QStringList header = rows.takeFirst();
|
|
const QMap<QString, QString> alias_map = headerAliasMap();
|
|
|
|
//The label line is followed by the machine header QElectroTech
|
|
//writes itself : the canonical names, so that the file is read the
|
|
//same way whatever the language it was written in.
|
|
if (!rows.isEmpty() && isMachineHeaderLine(header, rows.first(), alias_map)) {
|
|
rows.takeFirst();
|
|
}
|
|
|
|
QStringList used;
|
|
for (const QString &cell : header)
|
|
{
|
|
const QString column = canonicalColumn(cell, used, alias_map);
|
|
used.append(column);
|
|
data->columns.append(column);
|
|
}
|
|
|
|
for (const QStringList &row : std::as_const(rows))
|
|
{
|
|
MaterialRecord record;
|
|
bool empty = true;
|
|
for (int i = 0; i < data->columns.size(); ++i)
|
|
{
|
|
const QString value = i < row.size() ? row.at(i) : QString();
|
|
if (!value.isEmpty()) {
|
|
empty = false;
|
|
}
|
|
record.setValue(data->columns.at(i), value);
|
|
}
|
|
if (!empty) {
|
|
data->records.append(record);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::writeFile
|
|
Write a whole material file atomically : a crash or a full disk never
|
|
leaves a half written catalogue behind.
|
|
@param path
|
|
@param data
|
|
@param error receives a message when the file can't be written
|
|
@return true on success
|
|
*/
|
|
bool MaterialList::writeFile(const QString &path, const MaterialListData &data, QString *error)
|
|
{
|
|
if (error) {
|
|
error->clear();
|
|
}
|
|
|
|
QList<QStringList> rows;
|
|
rows.reserve(data.records.size());
|
|
for (const MaterialRecord &record : data.records)
|
|
{
|
|
QStringList row;
|
|
row.reserve(data.columns.size());
|
|
for (const QString &column : data.columns) {
|
|
row.append(record.value(column));
|
|
}
|
|
rows.append(row);
|
|
}
|
|
|
|
QList<QStringList> header_lines;
|
|
header_lines.append(translatedHeader(data.columns));
|
|
header_lines.append(data.columns);
|
|
const QByteArray content = serializeCsv(header_lines, rows, data.separator);
|
|
|
|
QSaveFile file(path);
|
|
if (!file.open(QIODevice::WriteOnly) || file.write(content) != content.size())
|
|
{
|
|
if (error) {
|
|
*error = file.errorString();
|
|
}
|
|
file.cancelWriting();
|
|
return false;
|
|
}
|
|
if (!file.commit())
|
|
{
|
|
if (error) {
|
|
*error = file.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::appendRecord
|
|
Add one article at the end of a material file.
|
|
The file is read again first: changes the user just saved from his
|
|
spreadsheet are part of what gets rewritten, never dropped.
|
|
@param path
|
|
@param record
|
|
@param error receives a message when the file can't be written
|
|
@return true on success
|
|
*/
|
|
bool MaterialList::appendRecord(const QString &path, const MaterialRecord &record, QString *error)
|
|
{
|
|
if (error) {
|
|
error->clear();
|
|
}
|
|
|
|
MaterialListData data;
|
|
if (hasContent(path))
|
|
{
|
|
if (!load(path, &data, error)) {
|
|
return false;
|
|
}
|
|
if (data.columns.isEmpty())
|
|
{
|
|
if (error) {
|
|
*error = QCoreApplication::translate("MaterialList", "Le fichier ne contient pas d'en-tête : colonnes manquantes.");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
data.columns = defaultColumns();
|
|
data.separator = QLatin1Char(';');
|
|
}
|
|
|
|
data.records.append(record);
|
|
return writeFile(path, data, error);
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::createFile
|
|
Create an empty material file holding only the header line.
|
|
@param path
|
|
@param error receives a message when the file can't be written
|
|
@return true on success
|
|
*/
|
|
bool MaterialList::createFile(const QString &path, QString *error)
|
|
{
|
|
if (error) {
|
|
error->clear();
|
|
}
|
|
|
|
if (hasContent(path))
|
|
{
|
|
if (error) {
|
|
*error = QCoreApplication::translate("MaterialList", "Le fichier existe déjà et n'est pas vide.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
MaterialListData data;
|
|
data.columns = defaultColumns();
|
|
data.separator = QLatin1Char(';');
|
|
return writeFile(path, data, error);
|
|
}
|
|
|
|
/**
|
|
@brief MaterialList::isEmptyFile
|
|
@param path
|
|
@return true when no file, or an empty one, exists at path
|
|
*/
|
|
bool MaterialList::isEmptyFile(const QString &path)
|
|
{
|
|
return !hasContent(path);
|
|
}
|