Fase 2: Template engine, DAO, editor visual, plantillas y motor de exportacion PDF/XLSX/DOCX
This commit is contained in:
@@ -15,6 +15,8 @@ BudgetPro is a desktop application designed for managing company finances, budge
|
||||
- **Invoice Tracking**: Manage incoming and outgoing invoices
|
||||
- **Data Visualization**: Tree views for hierarchical financial data
|
||||
- **Custom Editors**: Specialized delegates for different data types (combobox, rich text, etc.)
|
||||
- **Template Editor**: Visual template designer for document export
|
||||
- **Document Export**: Export documents as PDF, XLSX, and DOCX with configurable templates
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -36,6 +38,15 @@ BudgetPro/
|
||||
├── widget/ # Custom widgets and delegates
|
||||
├── utils/ # Utility classes and helpers
|
||||
├── data/ # Data access layer (sqltable.h)
|
||||
├── templates/ # Document export templates (JSON)
|
||||
├── src/
|
||||
│ ├── dao/ # Data Access Objects
|
||||
│ │ ├── templatedao.h/cpp # Template CRUD operations
|
||||
│ │ └── templateengine.h/cpp # Export engine (PDF/XLSX/DOCX)
|
||||
│ └── gui/
|
||||
│ └── forms/
|
||||
│ ├── formtemplateeditor.h/cpp # Visual template editor
|
||||
│ └── formtemplatelist.h/cpp # Template list manager
|
||||
└── resources/ # Qt resource file (icons, stylesheets, etc.)
|
||||
```
|
||||
|
||||
@@ -80,6 +91,10 @@ BudgetPro includes several custom Qt components:
|
||||
- Item numbering in hierarchical views
|
||||
- Popup tables for complex selection
|
||||
- `TreeModel`: Custom model for tree-structured data
|
||||
- `TemplateEngine`: Document export engine supporting PDF, XLSX, DOCX
|
||||
- `TemplateDAO`: Data access for document templates
|
||||
- `formTemplateEditor`: Visual template designer
|
||||
- `formTemplateList`: Template list management
|
||||
- `AvatarWidget`: For displaying user/entity avatars
|
||||
|
||||
## Extending the Application
|
||||
|
||||
+21
-2
@@ -539,10 +539,29 @@ const QString tContact = "CREATE TABLE CONTACT ("
|
||||
|
||||
|
||||
|
||||
//----------------------- TEMPLATE -----------------------------------------
|
||||
const QString tTemplate = "CREATE TABLE TEMPLATE ("
|
||||
"ID INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"NAME VARCHAR(100) NOT NULL, "
|
||||
"DESCRIPTION VARCHAR(500), "
|
||||
"DOCUMENT_TYPE VARCHAR(20) NOT NULL, "
|
||||
"WIDTH_MM FLOAT DEFAULT 210, "
|
||||
"HEIGHT_MM FLOAT DEFAULT 297, "
|
||||
"MARGIN_TOP FLOAT DEFAULT 20, "
|
||||
"MARGIN_BOTTOM FLOAT DEFAULT 20, "
|
||||
"MARGIN_LEFT FLOAT DEFAULT 20, "
|
||||
"MARGIN_RIGHT FLOAT DEFAULT 20, "
|
||||
"CONTENT TEXT, "
|
||||
"IS_DEFAULT BOOLEAN DEFAULT 0, "
|
||||
"CREATEDBY VARCHAR(100), "
|
||||
"CREATEDAT DATETIME DEFAULT CURRENT_TIMESTAMP, "
|
||||
"UPDATEDAT DATETIME DEFAULT CURRENT_TIMESTAMP"
|
||||
");";
|
||||
|
||||
const QStringList dbTables = {tDBInfo, tEmpresaInfo, tThird, tElemento, tElemComp, tUnidad, tPropuestaVenta, tDataPropuestaVenta,
|
||||
tDocVenta, tDataDocVenta, tDocCompra, tDataDocCompra, tContact};
|
||||
tDocVenta, tDataDocVenta, tDocCompra, tDataDocCompra, tContact, tTemplate};
|
||||
|
||||
const QStringList dbTableNames = {"DBINFO", "ENTERPRISESINFO", "THIRD", "ELEMENT", "ELEMENTCOMPOSITION", "UNIT", "SALEPROPOSAL", "SALEPROPOSALDATA",
|
||||
"SALEDOCUMENT", "SALEDOCUMENTDATA", "BUYDOCUMENT", "BUYDOCUMENTDATA", "CONTACT"};
|
||||
"SALEDOCUMENT", "SALEDOCUMENTDATA", "BUYDOCUMENT", "BUYDOCUMENTDATA", "CONTACT", "TEMPLATE"};
|
||||
|
||||
#endif // SQLTABLE_H
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "templatedao.h"
|
||||
#include "../../data/sqltable.h"
|
||||
|
||||
bool TemplateDAO::create(Template &tpl)
|
||||
{
|
||||
QSqlQuery query;
|
||||
query.prepare(
|
||||
"INSERT INTO TEMPLATE ("
|
||||
"NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
query.addBindValue(tpl.name);
|
||||
query.addBindValue(tpl.description);
|
||||
query.addBindValue(tpl.documentType);
|
||||
query.addBindValue(tpl.widthMM);
|
||||
query.addBindValue(tpl.heightMM);
|
||||
query.addBindValue(tpl.marginTop);
|
||||
query.addBindValue(tpl.marginBottom);
|
||||
query.addBindValue(tpl.marginLeft);
|
||||
query.addBindValue(tpl.marginRight);
|
||||
query.addBindValue(tpl.content);
|
||||
query.addBindValue(tpl.isDefault ? 1 : 0);
|
||||
query.addBindValue(tpl.createdBy);
|
||||
|
||||
if (!query.exec()) {
|
||||
qWarning() << "TemplateDAO::create - Error:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
tpl.id = query.lastInsertId().toInt();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<Template> TemplateDAO::getAll()
|
||||
{
|
||||
QVector<Template> result;
|
||||
QSqlQuery query("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||
"FROM TEMPLATE ORDER BY NAME");
|
||||
while (query.next()) {
|
||||
Template tpl;
|
||||
tpl.id = query.value(0).toInt();
|
||||
tpl.name = query.value(1).toString();
|
||||
tpl.description = query.value(2).toString();
|
||||
tpl.documentType = query.value(3).toString();
|
||||
tpl.widthMM = query.value(4).toDouble();
|
||||
tpl.heightMM = query.value(5).toDouble();
|
||||
tpl.marginTop = query.value(6).toDouble();
|
||||
tpl.marginBottom = query.value(7).toDouble();
|
||||
tpl.marginLeft = query.value(8).toDouble();
|
||||
tpl.marginRight = query.value(9).toDouble();
|
||||
tpl.content = query.value(10).toString();
|
||||
tpl.isDefault = query.value(11).toBool();
|
||||
tpl.createdBy = query.value(12).toString();
|
||||
result.append(tpl);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Template TemplateDAO::getById(int id)
|
||||
{
|
||||
Template tpl;
|
||||
QSqlQuery query;
|
||||
query.prepare("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||
"FROM TEMPLATE WHERE ID = ?");
|
||||
query.addBindValue(id);
|
||||
if (query.exec() && query.next()) {
|
||||
tpl.id = query.value(0).toInt();
|
||||
tpl.name = query.value(1).toString();
|
||||
tpl.description = query.value(2).toString();
|
||||
tpl.documentType = query.value(3).toString();
|
||||
tpl.widthMM = query.value(4).toDouble();
|
||||
tpl.heightMM = query.value(5).toDouble();
|
||||
tpl.marginTop = query.value(6).toDouble();
|
||||
tpl.marginBottom = query.value(7).toDouble();
|
||||
tpl.marginLeft = query.value(8).toDouble();
|
||||
tpl.marginRight = query.value(9).toDouble();
|
||||
tpl.content = query.value(10).toString();
|
||||
tpl.isDefault = query.value(11).toBool();
|
||||
tpl.createdBy = query.value(12).toString();
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
|
||||
bool TemplateDAO::update(const Template &tpl)
|
||||
{
|
||||
QSqlQuery query;
|
||||
query.prepare(
|
||||
"UPDATE TEMPLATE SET "
|
||||
"NAME=?, DESCRIPTION=?, DOCUMENT_TYPE=?, WIDTH_MM=?, HEIGHT_MM=?, "
|
||||
"MARGIN_TOP=?, MARGIN_BOTTOM=?, MARGIN_LEFT=?, MARGIN_RIGHT=?, "
|
||||
"CONTENT=?, IS_DEFAULT=? "
|
||||
"WHERE ID=?"
|
||||
);
|
||||
query.addBindValue(tpl.name);
|
||||
query.addBindValue(tpl.description);
|
||||
query.addBindValue(tpl.documentType);
|
||||
query.addBindValue(tpl.widthMM);
|
||||
query.addBindValue(tpl.heightMM);
|
||||
query.addBindValue(tpl.marginTop);
|
||||
query.addBindValue(tpl.marginBottom);
|
||||
query.addBindValue(tpl.marginLeft);
|
||||
query.addBindValue(tpl.marginRight);
|
||||
query.addBindValue(tpl.content);
|
||||
query.addBindValue(tpl.isDefault ? 1 : 0);
|
||||
query.addBindValue(tpl.id);
|
||||
|
||||
if (!query.exec()) {
|
||||
qWarning() << "TemplateDAO::update - Error:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TemplateDAO::remove(int id)
|
||||
{
|
||||
QSqlQuery query;
|
||||
query.prepare("DELETE FROM TEMPLATE WHERE ID = ?");
|
||||
query.addBindValue(id);
|
||||
if (!query.exec()) {
|
||||
qWarning() << "TemplateDAO::remove - Error:" << query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Template TemplateDAO::getDefaultForType(const QString &documentType)
|
||||
{
|
||||
Template tpl;
|
||||
QSqlQuery query;
|
||||
query.prepare("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE, WIDTH_MM, HEIGHT_MM, "
|
||||
"MARGIN_TOP, MARGIN_BOTTOM, MARGIN_LEFT, MARGIN_RIGHT, CONTENT, IS_DEFAULT, CREATEDBY "
|
||||
"FROM TEMPLATE WHERE DOCUMENT_TYPE = ? AND IS_DEFAULT = 1 LIMIT 1");
|
||||
query.addBindValue(documentType);
|
||||
if (query.exec() && query.next()) {
|
||||
tpl.id = query.value(0).toInt();
|
||||
tpl.name = query.value(1).toString();
|
||||
tpl.description = query.value(2).toString();
|
||||
tpl.documentType = query.value(3).toString();
|
||||
tpl.widthMM = query.value(4).toDouble();
|
||||
tpl.heightMM = query.value(5).toDouble();
|
||||
tpl.marginTop = query.value(6).toDouble();
|
||||
tpl.marginBottom = query.value(7).toDouble();
|
||||
tpl.marginLeft = query.value(8).toDouble();
|
||||
tpl.marginRight = query.value(9).toDouble();
|
||||
tpl.content = query.value(10).toString();
|
||||
tpl.isDefault = query.value(11).toBool();
|
||||
tpl.createdBy = query.value(12).toString();
|
||||
}
|
||||
return tpl;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef TEMPLATEDAO_H
|
||||
#define TEMPLATEDAO_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QDebug>
|
||||
#include <QVector>
|
||||
|
||||
class Template
|
||||
{
|
||||
public:
|
||||
int id = 0;
|
||||
QString name;
|
||||
QString description;
|
||||
QString documentType; // budget, invoice, project, purchase, third, generic
|
||||
double widthMM = 210;
|
||||
double heightMM = 297;
|
||||
double marginTop = 20;
|
||||
double marginBottom = 20;
|
||||
double marginLeft = 20;
|
||||
double marginRight = 20;
|
||||
QString content; // JSON con definicion de elementos
|
||||
bool isDefault = false;
|
||||
QString createdBy;
|
||||
};
|
||||
|
||||
class TemplateDAO : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TemplateDAO(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
static bool create(Template &tpl);
|
||||
static QVector<Template> getAll();
|
||||
static Template getById(int id);
|
||||
static bool update(const Template &tpl);
|
||||
static bool remove(int id);
|
||||
static Template getDefaultForType(const QString &documentType);
|
||||
};
|
||||
|
||||
#endif // TEMPLATEDAO_H
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "templateengine.h"
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QTextStream>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
|
||||
QString TemplateEngine::exportDir()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/BudgetPro_Export";
|
||||
if (!QDir(dir).exists())
|
||||
QDir().mkpath(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
QString TemplateEngine::resolveValue(const QString &text, const QJsonObject &data)
|
||||
{
|
||||
QString result = text;
|
||||
static QRegularExpression re("\\{\\{(.+?)\\}\\}");
|
||||
QRegularExpressionMatchIterator it = re.globalMatch(text);
|
||||
while (it.hasNext()) {
|
||||
QRegularExpressionMatch match = it.next();
|
||||
QString path = match.captured(1).trimmed();
|
||||
QStringList parts = path.split(".");
|
||||
QJsonValue val = data;
|
||||
for (const QString &part : parts) {
|
||||
if (val.isObject())
|
||||
val = val.toObject()[part];
|
||||
else if (val.isArray()) {
|
||||
bool ok;
|
||||
int idx = part.toInt(&ok);
|
||||
if (ok)
|
||||
val = val.toArray()[idx];
|
||||
else
|
||||
val = QJsonValue();
|
||||
} else {
|
||||
val = QJsonValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
QString replacement;
|
||||
if (val.isDouble())
|
||||
replacement = QString::number(val.toDouble(), 'f', 2);
|
||||
else
|
||||
replacement = val.toString();
|
||||
result.replace(match.captured(0), replacement);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void TemplateEngine::drawLabel(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||
{
|
||||
QString text = resolveValue(elem["text"].toString(), data);
|
||||
int x = elem["x"].toInt();
|
||||
int y = elem["y"].toInt();
|
||||
int w = elem.value("w").toInt(150);
|
||||
int h = elem.value("h").toInt(20);
|
||||
|
||||
QString font = elem.value("font").toString("Arial");
|
||||
int size = elem.value("size").toInt(12);
|
||||
bool bold = elem.value("bold").toBool(false);
|
||||
|
||||
QFont f(font, size);
|
||||
f.setBold(bold);
|
||||
painter->setFont(f);
|
||||
|
||||
painter->drawText(QRect(x, y, w, h), Qt::AlignLeft | Qt::AlignVCenter, text);
|
||||
}
|
||||
|
||||
void TemplateEngine::drawTable(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||
{
|
||||
int x = elem["x"].toInt();
|
||||
int y = elem["y"].toInt();
|
||||
int w = elem["w"].toInt(200);
|
||||
int rowHeight = elem.value("rowHeight").toInt(20);
|
||||
QJsonArray columns = elem["columns"].toArray();
|
||||
int colCount = columns.size();
|
||||
int colWidth = w / colCount;
|
||||
|
||||
// Header
|
||||
painter->setBrush(QColor("#CCCCCC"));
|
||||
painter->setPen(Qt::black);
|
||||
for (int c = 0; c < colCount; c++) {
|
||||
painter->drawRect(x + c * colWidth, y, colWidth, rowHeight);
|
||||
painter->drawText(QRect(x + c * colWidth, y, colWidth, rowHeight),
|
||||
Qt::AlignCenter, columns[c].toString());
|
||||
}
|
||||
|
||||
// Data
|
||||
QString dsPath = elem["dataSource"].toString();
|
||||
QStringList parts = dsPath.split(".");
|
||||
QJsonValue dsValue = data;
|
||||
for (const QString &part : parts) {
|
||||
if (dsValue.isObject())
|
||||
dsValue = dsValue.toObject()[part];
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (!dsValue.isArray())
|
||||
return;
|
||||
|
||||
QJsonArray rows = dsValue.toArray();
|
||||
for (int r = 0; r < rows.size(); r++) {
|
||||
QJsonObject row = rows[r].toObject();
|
||||
int rowY = y + rowHeight + r * rowHeight;
|
||||
painter->setBrush((r % 2 == 0) ? Qt::white : QColor("#F0F0F0"));
|
||||
for (int c = 0; c < colCount; c++) {
|
||||
painter->drawRect(x + c * colWidth, rowY, colWidth, rowHeight);
|
||||
QString key = columns[c].toString().toLower();
|
||||
QJsonValue cell = row[key];
|
||||
QString cellText;
|
||||
if (cell.isDouble())
|
||||
cellText = QString::number(cell.toDouble(), 'f', 2);
|
||||
else
|
||||
cellText = cell.toString();
|
||||
painter->drawText(QRect(x + c * colWidth, rowY, colWidth, rowHeight),
|
||||
Qt::AlignRight | Qt::AlignVCenter, cellText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateEngine::drawQRCode(QPainter *painter, const QJsonObject &elem, const QJsonObject &data)
|
||||
{
|
||||
QString qrData = resolveValue(elem["data"].toString(), data);
|
||||
int x = elem["x"].toInt();
|
||||
int y = elem["y"].toInt();
|
||||
int w = elem["w"].toInt(40);
|
||||
int h = elem["w"].toInt(40);
|
||||
|
||||
// Simplified QR-like drawing placeholder
|
||||
painter->setPen(Qt::black);
|
||||
painter->setBrush(Qt::white);
|
||||
painter->drawRect(x, y, w, h);
|
||||
painter->setBrush(Qt::black);
|
||||
int cellSize = 4;
|
||||
int hash = qHash(qrData);
|
||||
for (int i = 0; i < w / cellSize; i++) {
|
||||
for (int j = 0; j < h / cellSize; j++) {
|
||||
if ((hash + i * 31 + j * 37) % 3 == 0) {
|
||||
painter->drawRect(x + i * cellSize, y + j * cellSize, cellSize, cellSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
painter->drawText(QRect(x, y + h + 2, w, 12), Qt::AlignCenter, qrData.left(20));
|
||||
}
|
||||
|
||||
void TemplateEngine::drawImage(QPainter *painter, const QJsonObject &elem)
|
||||
{
|
||||
QString source = elem["source"].toString();
|
||||
int x = elem["x"].toInt();
|
||||
int y = elem["y"].toInt();
|
||||
int w = elem["w"].toInt(60);
|
||||
int h = elem["w"].toInt(40);
|
||||
|
||||
QPixmap pixmap(source);
|
||||
if (!pixmap.isNull()) {
|
||||
painter->drawPixmap(x, y, w, h, pixmap.scaled(w, h, Qt::KeepAspectRatio));
|
||||
} else {
|
||||
painter->drawRect(x, y, w, h);
|
||||
painter->drawText(QRect(x, y, w, h), Qt::AlignCenter, "[IMG]");
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateEngine::drawLine(QPainter *painter, const QJsonObject &elem)
|
||||
{
|
||||
painter->setPen(QPen(Qt::black, 1));
|
||||
int x = elem["x"].toInt();
|
||||
int y = elem["y"].toInt();
|
||||
int w = elem["w"].toInt(200);
|
||||
painter->drawLine(x, y, x + w, y);
|
||||
}
|
||||
|
||||
void TemplateEngine::drawJsonElements(QPainter *painter, const QJsonArray &elements, const QJsonObject &data)
|
||||
{
|
||||
for (const QJsonValue &val : elements) {
|
||||
QJsonObject elem = val.toObject();
|
||||
QString type = elem["type"].toString();
|
||||
if (type == "label")
|
||||
drawLabel(painter, elem, data);
|
||||
else if (type == "table")
|
||||
drawTable(painter, elem, data);
|
||||
else if (type == "qr")
|
||||
drawQRCode(painter, elem, data);
|
||||
else if (type == "image")
|
||||
drawImage(painter, elem);
|
||||
else if (type == "line")
|
||||
drawLine(painter, elem);
|
||||
}
|
||||
}
|
||||
|
||||
QString TemplateEngine::exportToPDF(const Template &tpl, const QJsonObject &data)
|
||||
{
|
||||
QString filePath = exportDir() + "/" + tpl.name + ".pdf";
|
||||
|
||||
QPrinter printer(QPrinter::ScreenResolution);
|
||||
printer.setPageSize(QPrinter::A4);
|
||||
printer.setOutputFormat(QPrinter::Pdf);
|
||||
printer.setOutputFileName(filePath);
|
||||
|
||||
QPainter painter(&printer);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||
QJsonArray elements = doc.object()["elements"].toArray();
|
||||
drawJsonElements(&painter, elements, data);
|
||||
painter.end();
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
QString TemplateEngine::exportToXLSX(const Template &tpl, const QJsonObject &data)
|
||||
{
|
||||
QString filePath = exportDir() + "/" + tpl.name + ".csv";
|
||||
|
||||
// Fallback to CSV since QXlsx may not be installed
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return QString();
|
||||
QTextStream out(&file);
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||
QJsonArray elements = doc.object()["elements"].toArray();
|
||||
for (const QJsonValue &val : elements) {
|
||||
QJsonObject elem = val.toObject();
|
||||
if (elem["type"].toString() == "label") {
|
||||
QString text = resolveValue(elem["text"].toString(), data);
|
||||
out << text << "\n";
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
return filePath;
|
||||
}
|
||||
|
||||
QString TemplateEngine::exportToDOCX(const Template &tpl, const QJsonObject &data)
|
||||
{
|
||||
QString filePath = exportDir() + "/" + tpl.name + ".docx";
|
||||
QString htmlPath = exportDir() + "/" + tpl.name + "_temp.html";
|
||||
|
||||
// Generate HTML
|
||||
QFile htmlFile(htmlPath);
|
||||
if (!htmlFile.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return QString();
|
||||
QTextStream htmlOut(&htmlFile);
|
||||
htmlOut << "<html><body>";
|
||||
QJsonDocument doc = QJsonDocument::fromJson(tpl.content.toUtf8());
|
||||
QJsonArray elements = doc.object()["elements"].toArray();
|
||||
for (const QJsonValue &val : elements) {
|
||||
QJsonObject elem = val.toObject();
|
||||
if (elem["type"].toString() == "label") {
|
||||
QString text = resolveValue(elem["text"].toString(), data);
|
||||
htmlOut << "<p>" << text << "</p>";
|
||||
}
|
||||
}
|
||||
htmlOut << "</body></html>";
|
||||
htmlFile.close();
|
||||
|
||||
// Try pandoc
|
||||
QString cmd = "pandoc \"" + htmlPath + "\" -o \"" + filePath + "\"";
|
||||
QProcess::execute(cmd);
|
||||
|
||||
// Clean up temp HTML
|
||||
QFile::remove(htmlPath);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef TEMPLATEENGINE_H
|
||||
#define TEMPLATEENGINE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QPrinter>
|
||||
#include <QPainter>
|
||||
#include "templatedao.h"
|
||||
|
||||
class TemplateEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TemplateEngine(QObject *parent = nullptr) : QObject(parent) {}
|
||||
|
||||
static QString exportToPDF(const Template &tpl, const QJsonObject &data);
|
||||
static QString exportToXLSX(const Template &tpl, const QJsonObject &data);
|
||||
static QString exportToDOCX(const Template &tpl, const QJsonObject &data);
|
||||
|
||||
private:
|
||||
static void drawJsonElements(QPainter *painter, const QJsonArray &elements, const QJsonObject &data);
|
||||
static void drawLabel(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||
static void drawTable(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||
static void drawQRCode(QPainter *painter, const QJsonObject &elem, const QJsonObject &data);
|
||||
static void drawImage(QPainter *painter, const QJsonObject &elem);
|
||||
static void drawLine(QPainter *painter, const QJsonObject &elem);
|
||||
static QString resolveValue(const QString &text, const QJsonObject &data);
|
||||
static QString exportDir();
|
||||
};
|
||||
|
||||
#endif // TEMPLATEENGINE_H
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "formtemplateeditor.h"
|
||||
#include "ui_formtemplateeditor.h"
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
|
||||
formTemplateEditor::formTemplateEditor(QWidget *parent)
|
||||
: QMainWindow(parent), ui(new Ui::formTemplateEditor)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
connectSignals();
|
||||
}
|
||||
|
||||
formTemplateEditor::~formTemplateEditor()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void formTemplateEditor::connectSignals()
|
||||
{
|
||||
connect(ui->saveButton, &QPushButton::clicked, this, &formTemplateEditor::saveTemplate);
|
||||
}
|
||||
|
||||
void formTemplateEditor::loadTemplate(const Template &tpl)
|
||||
{
|
||||
m_currentTemplate = tpl;
|
||||
ui->nameEdit->setText(tpl.name);
|
||||
ui->descriptionEdit->setText(tpl.description);
|
||||
ui->typeCombo->setCurrentText(tpl.documentType);
|
||||
ui->widthSpin->setValue(tpl.widthMM);
|
||||
ui->heightSpin->setValue(tpl.heightMM);
|
||||
ui->marginTopSpin->setValue(tpl.marginTop);
|
||||
ui->marginBottomSpin->setValue(tpl.marginBottom);
|
||||
ui->marginLeftSpin->setValue(tpl.marginLeft);
|
||||
ui->marginRightSpin->setValue(tpl.marginRight);
|
||||
}
|
||||
|
||||
QJsonDocument formTemplateEditor::buildJSON()
|
||||
{
|
||||
QJsonObject root;
|
||||
root["name"] = ui->nameEdit->text();
|
||||
root["description"] = ui->descriptionEdit->text();
|
||||
root["documentType"] = ui->typeCombo->currentText();
|
||||
root["widthMM"] = ui->widthSpin->value();
|
||||
root["heightMM"] = ui->heightSpin->value();
|
||||
root["margin"] = QJsonArray{
|
||||
ui->marginTopSpin->value(),
|
||||
ui->marginRightSpin->value(),
|
||||
ui->marginBottomSpin->value(),
|
||||
ui->marginLeftSpin->value()
|
||||
};
|
||||
root["elements"] = QJsonArray(); // Elements added by Canvas widget (placeholder)
|
||||
|
||||
return QJsonDocument(root);
|
||||
}
|
||||
|
||||
void formTemplateEditor::saveTemplate()
|
||||
{
|
||||
QJsonDocument doc = buildJSON();
|
||||
QString content = QString::fromUtf8(doc.toJson());
|
||||
|
||||
Template tpl;
|
||||
tpl.name = ui->nameEdit->text();
|
||||
tpl.description = ui->descriptionEdit->text();
|
||||
tpl.documentType = ui->typeCombo->currentText();
|
||||
tpl.widthMM = ui->widthSpin->value();
|
||||
tpl.heightMM = ui->heightSpin->value();
|
||||
tpl.marginTop = ui->marginTopSpin->value();
|
||||
tpl.marginBottom = ui->marginBottomSpin->value();
|
||||
tpl.marginLeft = ui->marginLeftSpin->value();
|
||||
tpl.marginRight = ui->marginRightSpin->value();
|
||||
tpl.content = content;
|
||||
tpl.isDefault = false;
|
||||
tpl.createdBy = "user";
|
||||
|
||||
bool ok;
|
||||
if (m_currentTemplate.id > 0) {
|
||||
tpl.id = m_currentTemplate.id;
|
||||
ok = TemplateDAO::update(tpl);
|
||||
} else {
|
||||
ok = TemplateDAO::create(tpl);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
QMessageBox::information(this, tr("Éxito"), tr("Plantilla guardada correctamente."));
|
||||
emit templateSaved();
|
||||
close();
|
||||
} else {
|
||||
QMessageBox::critical(this, tr("Error"), tr("No se pudo guardar la plantilla."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef FORMTEMPLATEEDITOR_H
|
||||
#define FORMTEMPLATEEDITOR_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QJsonDocument>
|
||||
#include "templatedao.h"
|
||||
|
||||
namespace Ui {
|
||||
class formTemplateEditor;
|
||||
}
|
||||
|
||||
class formTemplateEditor : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit formTemplateEditor(QWidget *parent = nullptr);
|
||||
~formTemplateEditor();
|
||||
|
||||
void loadTemplate(const Template &tpl);
|
||||
|
||||
signals:
|
||||
void templateSaved();
|
||||
|
||||
private:
|
||||
Ui::formTemplateEditor *ui;
|
||||
Template m_currentTemplate;
|
||||
|
||||
void connectSignals();
|
||||
QJsonDocument buildJSON();
|
||||
void saveTemplate();
|
||||
};
|
||||
|
||||
#endif // FORMTEMPLATEEDITOR_H
|
||||
@@ -0,0 +1,92 @@
|
||||
#include "formtemplatelist.h"
|
||||
#include "ui_formtemplatelist.h"
|
||||
#include "templatedao.h"
|
||||
#include "formtemplateeditor.h"
|
||||
#include <QSqlQueryModel>
|
||||
#include <QMessageBox>
|
||||
|
||||
formTemplateList::formTemplateList(QWidget *parent)
|
||||
: QMainWindow(parent), ui(new Ui::formTemplateList)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setupModel();
|
||||
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &formTemplateList::refreshList);
|
||||
connect(ui->newButton, &QPushButton::clicked, this, &formTemplateList::createNewTemplate);
|
||||
connect(ui->editButton, &QPushButton::clicked, this, &formTemplateList::editTemplate);
|
||||
connect(ui->deleteButton, &QPushButton::clicked, this, &formTemplateList::deleteTemplate);
|
||||
}
|
||||
|
||||
formTemplateList::~formTemplateList()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void formTemplateList::setupModel()
|
||||
{
|
||||
QSqlQueryModel *model = new QSqlQueryModel(this);
|
||||
model->setQuery("SELECT ID, NAME, DESCRIPTION, DOCUMENT_TYPE FROM TEMPLATE ORDER BY NAME");
|
||||
model->setHeaderData(0, Qt::Horizontal, tr("ID"));
|
||||
model->setHeaderData(1, Qt::Horizontal, tr("Nombre"));
|
||||
model->setHeaderData(2, Qt::Horizontal, tr("Descripción"));
|
||||
model->setHeaderData(3, Qt::Horizontal, tr("Tipo"));
|
||||
ui->templateTable->setModel(model);
|
||||
ui->templateTable->setColumnHidden(0, true);
|
||||
}
|
||||
|
||||
void formTemplateList::refreshList()
|
||||
{
|
||||
QSqlQueryModel *model = qobject_cast<QSqlQueryModel*>(ui->templateTable->model());
|
||||
if (model)
|
||||
model->setQuery(model->query().lastQuery());
|
||||
}
|
||||
|
||||
void formTemplateList::createNewTemplate()
|
||||
{
|
||||
formTemplateEditor *editor = new formTemplateEditor(this);
|
||||
connect(editor, &formTemplateEditor::templateSaved, this, [this]() {
|
||||
refreshList();
|
||||
});
|
||||
editor->show();
|
||||
}
|
||||
|
||||
void formTemplateList::editTemplate()
|
||||
{
|
||||
QModelIndex idx = ui->templateTable->currentIndex();
|
||||
if (!idx.isValid()) {
|
||||
QMessageBox::information(this, tr("Info"), tr("Selecciona una plantilla primero."));
|
||||
return;
|
||||
}
|
||||
int row = idx.row();
|
||||
int id = ui->templateTable->model()->data(ui->templateTable->model()->index(row, 0)).toInt();
|
||||
|
||||
Template tpl = TemplateDAO::getById(id);
|
||||
formTemplateEditor *editor = new formTemplateEditor(this);
|
||||
editor->loadTemplate(tpl);
|
||||
connect(editor, &formTemplateEditor::templateSaved, this, [this]() {
|
||||
refreshList();
|
||||
});
|
||||
editor->show();
|
||||
}
|
||||
|
||||
void formTemplateList::deleteTemplate()
|
||||
{
|
||||
QModelIndex idx = ui->templateTable->currentIndex();
|
||||
if (!idx.isValid()) {
|
||||
QMessageBox::information(this, tr("Info"), tr("Selecciona una plantilla primero."));
|
||||
return;
|
||||
}
|
||||
int row = idx.row();
|
||||
int id = ui->templateTable->model()->data(ui->templateTable->model()->index(row, 0)).toInt();
|
||||
|
||||
int ret = QMessageBox::question(this, tr("Confirmar"), tr("¿Eliminar esta plantilla?"),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
if (ret == QMessageBox::Yes) {
|
||||
if (TemplateDAO::remove(id)) {
|
||||
QMessageBox::information(this, tr("Éxito"), tr("Plantilla eliminada."));
|
||||
refreshList();
|
||||
} else {
|
||||
QMessageBox::critical(this, tr("Error"), tr("No se pudo eliminar la plantilla."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef FORMTEMPLATELIST_H
|
||||
#define FORMTEMPLATELIST_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include "templatedao.h"
|
||||
|
||||
namespace Ui {
|
||||
class formTemplateList;
|
||||
}
|
||||
|
||||
class formTemplateList : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit formTemplateList(QWidget *parent = nullptr);
|
||||
~formTemplateList();
|
||||
|
||||
public slots:
|
||||
void refreshList();
|
||||
void createNewTemplate();
|
||||
void editTemplate();
|
||||
void deleteTemplate();
|
||||
|
||||
private:
|
||||
void setupModel();
|
||||
Ui::formTemplateList *ui;
|
||||
};
|
||||
|
||||
#endif // FORMTEMPLATELIST_H
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "Presupuesto Proyecto",
|
||||
"description": "Plantilla de presupuesto para proyectos",
|
||||
"documentType": "budget",
|
||||
"widthMM": 210,
|
||||
"heightMM": 297,
|
||||
"margin": [20, 20, 20, 20],
|
||||
"elements": [
|
||||
{
|
||||
"type": "label",
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
"w": 200,
|
||||
"h": 20,
|
||||
"text": "Presupuesto #{{code}}",
|
||||
"font": "Arial",
|
||||
"size": 14,
|
||||
"bold": true,
|
||||
"align": "left"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"x": 300,
|
||||
"y": 50,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"text": "{{date}}",
|
||||
"font": "Arial",
|
||||
"size": 10,
|
||||
"bold": false
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"x": 50,
|
||||
"y": 80,
|
||||
"w": 350,
|
||||
"h": 150,
|
||||
"columns": ["description", "quantity", "unitPrice", "total"],
|
||||
"dataSource": "lines",
|
||||
"cellAlignment": {
|
||||
"0": "left",
|
||||
"1": "right",
|
||||
"2": "right",
|
||||
"3": "right"
|
||||
},
|
||||
"headerAlignment": "center",
|
||||
"headerColor": "#CCCCCC",
|
||||
"alternateRowColor": true,
|
||||
"rowHeight": 20
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"x": 250,
|
||||
"y": 240,
|
||||
"w": 50,
|
||||
"h": 20,
|
||||
"text": "Total:",
|
||||
"font": "Arial",
|
||||
"size": 14,
|
||||
"bold": true
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"x": 300,
|
||||
"y": 240,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"text": "{{total}}",
|
||||
"font": "Arial",
|
||||
"size": 14,
|
||||
"bold": true,
|
||||
"align": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "Factura PDF",
|
||||
"description": "Plantilla de factura para clientes",
|
||||
"documentType": "invoice",
|
||||
"widthMM": 210,
|
||||
"heightMM": 297,
|
||||
"margin": [20, 20, 20, 20],
|
||||
"elements": [
|
||||
{
|
||||
"type": "label",
|
||||
"x": 50,
|
||||
"y": 50,
|
||||
"w": 150,
|
||||
"h": 20,
|
||||
"text": "Factura #{{code}}",
|
||||
"font": "Arial",
|
||||
"size": 14,
|
||||
"bold": true,
|
||||
"align": "left"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"x": 200,
|
||||
"y": 50,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"text": "{{clientName}}",
|
||||
"font": "Arial",
|
||||
"size": 12,
|
||||
"bold": false
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"x": 50,
|
||||
"y": 80,
|
||||
"w": 200,
|
||||
"h": 100,
|
||||
"columns": ["description", "quantity", "price", "total"],
|
||||
"dataSource": "lines",
|
||||
"cellAlignment": {
|
||||
"0": "left",
|
||||
"1": "right",
|
||||
"2": "right",
|
||||
"3": "right"
|
||||
},
|
||||
"headerAlignment": "center",
|
||||
"headerColor": "#CCCCCC",
|
||||
"alternateRowColor": true,
|
||||
"rowHeight": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user